diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 27aa99120d..64aec703df 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -25,6 +25,11 @@ import { getThreadSelectorsFromThreadsKey, selectedThreadSelectors, } from 'firefox-profiler/selectors/per-thread'; +import { + getProfileFlowInfo, + getStringTablePerThread, + getFullMarkerListPerThread, +} from 'firefox-profiler/selectors/flow'; import { getAllCommittedRanges, getImplementationFilter, @@ -79,11 +84,13 @@ import type { TableViewOptions, SelectionContext, BottomBoxInfo, + IndexIntoFlowTable, } from 'firefox-profiler/types'; import { funcHasDirectRecursiveCall, funcHasRecursiveCall, } from '../profile-logic/transforms'; +import { computeMarkerFlows } from '../profile-logic/marker-data'; import { changeStoredProfileNameInDb } from 'firefox-profiler/app-logic/uploaded-profiles-db'; import type { TabSlug } from '../app-logic/tabs-handling'; import { intersectSets } from 'firefox-profiler/utils/set'; @@ -1727,6 +1734,37 @@ export function changeHoveredMarker( }; } +export function changeActiveFlows(activeFlows: IndexIntoFlowTable[]): Action { + return { + type: 'CHANGE_ACTIVE_FLOWS', + activeFlows, + }; +} + +export function activateFlowsForMarker( + threadIndex: ThreadIndex, + markerIndex: MarkerIndex +): ThunkAction { + console.log('yo'); + return (dispatch, getState) => { + console.log('aha'); + const profileFlowInfo = getProfileFlowInfo(getState()); + const stringTablePerThread = getStringTablePerThread(getState()); + const fullMarkerListPerThread = getFullMarkerListPerThread(getState()); + console.log('aha2'); + const flows = + computeMarkerFlows( + threadIndex, + markerIndex, + profileFlowInfo, + stringTablePerThread, + fullMarkerListPerThread + ) ?? []; + console.log({ flows }); + dispatch(changeActiveFlows(flows)); + }; +} + /** * This action is used when the user right clicks a marker, and is especially * used to display its context menu. diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index f2ce716633..b90790e3ec 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -186,6 +186,7 @@ type BaseQuery = {| timelineType: string, sourceView: string, assemblyView: string, + activeFlows: string, ...FullProfileSpecificBaseQuery, ...ActiveTabProfileSpecificBaseQuery, ...OriginsProfileSpecificBaseQuery, @@ -436,6 +437,9 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { query = (baseQuery: MarkersQueryShape); query.markerSearch = urlState.profileSpecific.markersSearchString || undefined; + query.activeFlows = + encodeUintArrayForUrlComponent(urlState.profileSpecific.activeFlows) || + undefined; break; case 'network-chart': query = (baseQuery: NetworkQueryShape); @@ -578,6 +582,8 @@ export function stateFromLocation( implementation = query.implementation; } + const activeFlows = decodeUintArrayFromUrlComponent(query.activeFlows ?? ''); + const transforms = {}; if (selectedThreadsKey !== null) { transforms[selectedThreadsKey] = parseTransforms(query.transforms); @@ -658,6 +664,7 @@ export function stateFromLocation( transforms, sourceView, assemblyView, + activeFlows, isBottomBoxOpenPerPanel, timelineType: validateTimelineType(query.timelineType), full: { diff --git a/src/components/app/Details.js b/src/components/app/Details.js index 193a194d02..ce9b5c4564 100644 --- a/src/components/app/Details.js +++ b/src/components/app/Details.js @@ -14,7 +14,7 @@ import { LocalizedErrorBoundary } from './ErrorBoundary'; import { ProfileCallTreeView } from 'firefox-profiler/components/calltree/ProfileCallTreeView'; import { MarkerTable } from 'firefox-profiler/components/marker-table'; import { StackChart } from 'firefox-profiler/components/stack-chart/'; -import { MarkerChart } from 'firefox-profiler/components/marker-chart/'; +import { MarkerChartTab } from 'firefox-profiler/components/marker-chart-tab'; import { NetworkChart } from 'firefox-profiler/components/network-chart/'; import { FlameGraph } from 'firefox-profiler/components/flame-graph/'; import { JsTracer } from 'firefox-profiler/components/js-tracer/'; @@ -115,7 +115,7 @@ class ProfileViewerImpl extends PureComponent { calltree: , 'flame-graph': , 'stack-chart': , - 'marker-chart': , + 'marker-chart': , 'marker-table': , 'network-chart': , 'js-tracer': , diff --git a/src/components/app/DetailsContainer.css b/src/components/app/DetailsContainer.css index f46c2679eb..41b236a392 100644 --- a/src/components/app/DetailsContainer.css +++ b/src/components/app/DetailsContainer.css @@ -1,10 +1,10 @@ -.DetailsContainer .layout-pane > * { +.DetailsContainer > .layout-pane > * { width: 100%; height: 100%; box-sizing: border-box; } -.DetailsContainer .layout-pane:not(.layout-pane-primary) { +.DetailsContainer > .layout-pane:not(.layout-pane-primary) { max-width: 600px; } @@ -15,12 +15,12 @@ position: unset; } -.DetailsContainer .layout-splitter { +.DetailsContainer > .layout-splitter { border-top: 1px solid var(--grey-30); border-left: 1px solid var(--grey-30); background: var(--grey-10); /* Same background as sidebars */ } -.DetailsContainer .layout-splitter:hover { +.DetailsContainer > .layout-splitter:hover { background: var(--grey-30); /* same as the border above */ } diff --git a/src/components/flow-panel/Canvas.js b/src/components/flow-panel/Canvas.js new file mode 100644 index 0000000000..478bf2673e --- /dev/null +++ b/src/components/flow-panel/Canvas.js @@ -0,0 +1,798 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow +import { GREY_20, BLUE_60, BLUE_80 } from 'photon-colors'; +import * as React from 'react'; +import { + withChartViewport, + type WithChartViewport, + type Viewport, +} from 'firefox-profiler/components/shared/chart/Viewport'; +import { ChartCanvas } from 'firefox-profiler/components/shared/chart/Canvas'; +import { TooltipMarker } from 'firefox-profiler/components/tooltip/Marker'; +import TextMeasurement from 'firefox-profiler/utils/text-measurement'; +import { bisectionRight } from 'firefox-profiler/utils/bisect'; +import { + typeof updatePreviewSelection as UpdatePreviewSelection, + typeof changeMouseTimePosition as ChangeMouseTimePosition, + typeof changeActiveFlows as ChangeActiveFlows, +} from 'firefox-profiler/actions/profile-view'; +import { TIMELINE_MARGIN_LEFT } from 'firefox-profiler/app-logic/constants'; +import type { + Milliseconds, + CssPixels, + UnitIntervalOfProfileRange, + Marker, + MarkerIndex, + FlowTiming, + ThreadIndex, + IndexIntoFlowTable, + FlowTimingRow, +} from 'firefox-profiler/types'; +import { getStartEndRangeForMarker } from 'firefox-profiler/utils'; +import { ensureExists } from 'firefox-profiler/utils/flow'; + +import type { + ChartCanvasScale, + ChartCanvasHoverInfo, +} from '../shared/chart/Canvas'; + +import type { WrapFunctionInDispatch } from 'firefox-profiler/utils/connect'; + +type HoveredFlowPanelItems = {| + rowIndex: number | null, + flowIndex: IndexIntoFlowTable | null, + indexInFlowMarkers: number | null, // index into flows[flowIndex].flowMarkers + threadIndex: ThreadIndex | null, + markerIndex: MarkerIndex | null, +|}; + +type OwnProps = {| + +rangeStart: Milliseconds, + +rangeEnd: Milliseconds, + +flowTiming: FlowTiming, + +rowHeight: CssPixels, + +fullMarkerListPerThread: Marker[][], + +markerLabelGetterPerThread: Array<(MarkerIndex) => string>, + +updatePreviewSelection: WrapFunctionInDispatch, + +changeMouseTimePosition: ChangeMouseTimePosition, + +changeActiveFlows: ChangeActiveFlows, + +marginLeft: CssPixels, + +marginRight: CssPixels, + +shouldDisplayTooltips: () => boolean, +|}; + +type Props = {| + ...OwnProps, + // Bring in the viewport props from the higher order Viewport component. + +viewport: Viewport, +|}; + +const TEXT_OFFSET_TOP = 11; +const TEXT_OFFSET_START = 3; +const MARKER_DOT_RADIUS = 0.25; +const LABEL_PADDING = 5; +const MARKER_BORDER_COLOR = '#2c77d1'; + +class FlowPanelCanvasImpl extends React.PureComponent { + _textMeasurement: null | TextMeasurement; + + drawCanvas = ( + ctx: CanvasRenderingContext2D, + scale: ChartCanvasScale, + _hoverInfo: ChartCanvasHoverInfo + ) => { + const { + rowHeight, + flowTiming, + viewport: { + viewportTop, + viewportBottom, + containerWidth, + containerHeight, + }, + } = this.props; + const { cssToUserScale } = scale; + if (cssToUserScale !== 1) { + throw new Error( + 'StackChartCanvasImpl sets scaleCtxToCssPixels={true}, so canvas user space units should be equal to CSS pixels.' + ); + } + + // Convert CssPixels to Stack Depth + const rowCount = flowTiming.rows.length; + const startRow = Math.floor(viewportTop / rowHeight); + const endRow = Math.min(Math.ceil(viewportBottom / rowHeight), rowCount); + + // Common properties that won't be changed later. + ctx.lineWidth = 1; + + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, containerWidth, containerHeight); + this.drawRowHighlights(ctx, startRow, endRow); + this.drawRowContents(ctx, null, startRow, endRow); + this.drawSeparatorsAndLabels(ctx, startRow, endRow); + }; + + drawRowHighlights( + ctx: CanvasRenderingContext2D, + startRow: number, + endRow: number + ) { + const { flowTiming } = this.props; + const { rows } = flowTiming; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + const rowType = rows[rowIndex].rowType; + if (rowType === 'ACTIVE') { + this.drawRowHighlight(ctx, rowIndex); + } + } + } + + drawRowHighlight(ctx: CanvasRenderingContext2D, rowIndex: number) { + const { + rowHeight, + viewport: { viewportTop, containerWidth }, + } = this.props; + + ctx.fillStyle = 'rgba(40, 122, 169, 0.2)'; + ctx.fillRect( + 0, // To include the labels also + rowIndex * rowHeight - viewportTop, + containerWidth, + rowHeight - 1 // Subtract 1 for borders. + ); + } + + drawFlowRectangle( + ctx: CanvasRenderingContext2D, + rowIndex: number, + timeAtViewportLeft: number, + timeAtViewportRightPlusMargin: number, + rangeStart: number, + rangeLength: number, + viewportLeft: number, + markerContainerWidth: number, + viewportLength: number, + marginLeft: number + ) { + const { + rowHeight, + flowTiming, + viewport: { viewportTop }, + } = this.props; + const { rows } = flowTiming; + const { devicePixelRatio } = window; + + const row = rows[rowIndex]; + const startTimestamp = row.flowStart; + const endTimestamp = row.flowEnd; + + const y: CssPixels = rowIndex * rowHeight - viewportTop; + const h: CssPixels = rowHeight - 1; + + // Only draw samples that are in bounds. + if ( + !( + endTimestamp >= timeAtViewportLeft && + startTimestamp < timeAtViewportRightPlusMargin + ) + ) { + return; + } + const startTime: UnitIntervalOfProfileRange = + (startTimestamp - rangeStart) / rangeLength; + const endTime: UnitIntervalOfProfileRange = + (endTimestamp - rangeStart) / rangeLength; + + let x: CssPixels = + ((startTime - viewportLeft) * markerContainerWidth) / viewportLength + + marginLeft; + let w: CssPixels = + ((endTime - startTime) * markerContainerWidth) / viewportLength; + + x = Math.round(x * devicePixelRatio) / devicePixelRatio; + w = Math.round(w * devicePixelRatio) / devicePixelRatio; + + ctx.strokeStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.beginPath(); + + // We want the rectangle to have a clear margin, that's why we increment y + // and decrement h (twice, for both margins). + // We also add "0.5" more so that the stroke is properly on a pixel. + // Indeed strokes are drawn on both sides equally, so half a pixel on each + // side in this case. + ctx.rect( + x + 0.5, // + 0.5 for the stroke + y + 1 + 0.5, // + 1 for the top margin, + 0.5 for the stroke + w - 1, // - 1 to account for left and right strokes. + h - 2 - 1 // + 2 accounts for top and bottom margins, + 1 accounts for top and bottom strokes + ); + ctx.stroke(); + } + + // Note: we used a long argument list instead of an object parameter on + // purpose, to reduce GC pressure while drawing. + drawOneMarker( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + w: CssPixels, + h: CssPixels, + isInstantMarker: boolean, + markerIndex: MarkerIndex, + threadIndex: number, + isHighlighted: boolean = false + ) { + if (isInstantMarker) { + this.drawOneInstantMarker(ctx, x, y, h, isHighlighted); + } else { + this.drawOneIntervalMarker( + ctx, + x, + y, + w, + h, + markerIndex, + threadIndex, + isHighlighted + ); + } + } + + drawOneIntervalMarker( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + w: CssPixels, + h: CssPixels, + markerIndex: MarkerIndex, + threadIndex: number, + isHighlighted: boolean + ) { + const { marginLeft, markerLabelGetterPerThread } = this.props; + + if (w <= 2) { + // This is an interval marker small enough that if we drew it as a + // rectangle, we wouldn't see any inside part. With a width of 2 pixels, + // the rectangle-with-borders would only be borders. With less than 2 + // pixels, the borders would collapse. + // So let's draw it directly as a rect. + ctx.fillStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; + + // w is rounded in the caller, but let's make sure it's at least 1. + w = Math.max(w, 1); + ctx.fillRect(x, y + 1, w, h - 2); + } else { + // This is a bigger interval marker. + const textMeasurement = this._getTextMeasurement(ctx); + + ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff'; + ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; + + ctx.beginPath(); + + // We want the rectangle to have a clear margin, that's why we increment y + // and decrement h (twice, for both margins). + // We also add "0.5" more so that the stroke is properly on a pixel. + // Indeed strokes are drawn on both sides equally, so half a pixel on each + // side in this case. + ctx.rect( + x + 0.5, // + 0.5 for the stroke + y + 1 + 0.5, // + 1 for the top margin, + 0.5 for the stroke + w - 1, // - 1 to account for left and right strokes. + h - 2 - 1 // + 2 accounts for top and bottom margins, + 1 accounts for top and bottom strokes + ); + ctx.fill(); + ctx.stroke(); + + // Draw the text label + // TODO - L10N RTL. + // Constrain the x coordinate to the leftmost area. + const x2: CssPixels = + x < marginLeft ? marginLeft + TEXT_OFFSET_START : x + TEXT_OFFSET_START; + const visibleWidth = x < marginLeft ? w - marginLeft + x : w; + const w2: CssPixels = visibleWidth - 2 * TEXT_OFFSET_START; + + if (w2 > textMeasurement.minWidth) { + const fittedText = textMeasurement.getFittedText( + markerLabelGetterPerThread[threadIndex](markerIndex), + w2 + ); + if (fittedText) { + ctx.fillStyle = isHighlighted ? 'white' : 'black'; + ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); + } + } + } + } + + // x indicates the center of this marker + // y indicates the top of the row + // h indicates the available height in the row + drawOneInstantMarker( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + h: CssPixels, + isHighlighted: boolean + ) { + ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff'; + ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; + + // We're drawing a diamond shape, whose height is h - 2, and width is h / 2. + ctx.beginPath(); + ctx.moveTo(x - h / 4, y + h / 2); + ctx.lineTo(x, y + 1.5); + ctx.lineTo(x + h / 4, y + h / 2); + ctx.lineTo(x, y + h - 1.5); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + + drawMarkersForRow( + ctx: CanvasRenderingContext2D, + rowIndex: number, + flowTimingRow: FlowTimingRow, + timeAtViewportLeft: number, + timeAtViewportRightPlusMargin: number, + rangeStart: Milliseconds, + rangeLength: Milliseconds, + viewportLeft: CssPixels, + viewportLength: CssPixels, + rowHeight: CssPixels, + viewportTop: CssPixels, + markerContainerWidth: CssPixels, + marginLeft: CssPixels + ) { + const { devicePixelRatio } = window; + + const { markers } = flowTimingRow; + + const y: CssPixels = rowIndex * rowHeight - viewportTop; + const h: CssPixels = rowHeight - 1; + + // Track the last drawn marker X position, so that we can avoid overdrawing. + let previousMarkerDrawnAtX: number | null = null; + + for (let i = 0; i < markers.length; i++) { + const startTimestamp = markers.startTime[i]; + const endTimestamp = markers.endTime[i]; + const isInstantMarker = markers.isInstant[i] === 1; + + // Only draw samples that are in bounds. + if ( + endTimestamp >= timeAtViewportLeft && + startTimestamp < timeAtViewportRightPlusMargin + ) { + const startTime: UnitIntervalOfProfileRange = + (startTimestamp - rangeStart) / rangeLength; + const endTime: UnitIntervalOfProfileRange = + (endTimestamp - rangeStart) / rangeLength; + + let x: CssPixels = + ((startTime - viewportLeft) * markerContainerWidth) / viewportLength + + marginLeft; + let w: CssPixels = + ((endTime - startTime) * markerContainerWidth) / viewportLength; + + x = Math.round(x * devicePixelRatio) / devicePixelRatio; + w = Math.round(w * devicePixelRatio) / devicePixelRatio; + + const markerIndex = markers.markerIndex[i]; + const threadIndex = markers.threadIndex[i]; + + if ( + // Always render non-dot markers and markers that are larger than + // one pixel. + w > 1 || + // Do not render dot markers that occupy the same pixel, as this can take + // a lot of time, and not change the visual display of the chart. + x !== previousMarkerDrawnAtX + ) { + previousMarkerDrawnAtX = x; + this.drawOneMarker( + ctx, + x, + y, + w, + h, + isInstantMarker, + markerIndex, + threadIndex + ); + } + } + } + } + + drawRowContents( + ctx: CanvasRenderingContext2D, + hoveredItem: MarkerIndex | null, + startRow: number, + endRow: number + ) { + const { + rangeStart, + rangeEnd, + flowTiming, + rowHeight, + marginLeft, + marginRight, + viewport: { + containerWidth, + containerHeight, + viewportLeft, + viewportRight, + viewportTop, + }, + } = this.props; + + const markerContainerWidth = containerWidth - marginLeft - marginRight; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + + // Decide which samples to actually draw + const timeAtViewportLeft: Milliseconds = + rangeStart + rangeLength * viewportLeft; + const timeAtViewportRightPlusMargin: Milliseconds = + rangeStart + + rangeLength * viewportRight + + // This represents the amount of seconds in the right margin: + marginRight * ((viewportLength * rangeLength) / markerContainerWidth); + + // We'll restore the context at the end, so that the clip region will be + // removed. + ctx.save(); + // The clip operation forbids drawing in the label zone. + ctx.beginPath(); + ctx.rect(marginLeft, 0, markerContainerWidth, containerHeight); + ctx.clip(); + + // 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 flowTimingRow = flowTiming.rows[rowIndex]; + this.drawFlowRectangle( + ctx, + rowIndex, + timeAtViewportLeft, + timeAtViewportRightPlusMargin, + rangeStart, + rangeLength, + viewportLeft, + markerContainerWidth, + viewportLength, + marginLeft + ); + this.drawMarkersForRow( + ctx, + rowIndex, + flowTimingRow, + timeAtViewportLeft, + timeAtViewportRightPlusMargin, + rangeStart, + rangeLength, + viewportLeft, + viewportLength, + rowHeight, + viewportTop, + markerContainerWidth, + marginLeft + ); + } + + ctx.restore(); + } + + /** + * Lazily create the text measurement tool, as a valid 2d rendering context must + * exist before it is created. + */ + _getTextMeasurement(ctx: CanvasRenderingContext2D): TextMeasurement { + if (!this._textMeasurement) { + this._textMeasurement = new TextMeasurement(ctx); + } + return this._textMeasurement; + } + + drawSeparatorsAndLabels( + ctx: CanvasRenderingContext2D, + startRow: number, + endRow: number + ) { + const { + flowTiming, + rowHeight, + marginLeft, + marginRight, + viewport: { viewportTop, containerWidth, containerHeight }, + } = this.props; + + const usefulContainerWidth = containerWidth - marginRight; + + // Draw separators + ctx.fillStyle = GREY_20; + ctx.fillRect(marginLeft - 1, 0, 1, containerHeight); + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // `- 1` at the end, because the top separator is not drawn in the canvas, + // it's drawn using CSS' border property. And canvas positioning is 0-based. + const y = (rowIndex + 1) * rowHeight - viewportTop - 1; + ctx.fillRect(0, y, usefulContainerWidth, 1); + } + + const textMeasurement = this._getTextMeasurement(ctx); + + // Draw the marker names in the left margin. + ctx.fillStyle = '#000000'; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + const markerTimingRow = flowTiming.rows[rowIndex]; + // Draw the marker name. + const { label } = markerTimingRow; + + const y = rowIndex * rowHeight - viewportTop; + + // Even when it's on active tab view, have a hard cap on the text length. + const fittedText = textMeasurement.getFittedText( + label, + TIMELINE_MARGIN_LEFT - LABEL_PADDING + ); + + ctx.fillText(fittedText, LABEL_PADDING, y + TEXT_OFFSET_TOP); + } + } + + hitTest = (x: CssPixels, y: CssPixels): HoveredFlowPanelItems | null => { + const { + rangeStart, + rangeEnd, + flowTiming, + rowHeight, + marginLeft, + marginRight, + viewport: { viewportLeft, viewportRight, viewportTop, containerWidth }, + } = this.props; + + // Note: we may want to increase this value to hit markers that are farther. + const dotRadius: CssPixels = MARKER_DOT_RADIUS * rowHeight; + if (x < marginLeft - dotRadius) { + return null; + } + + let markerIndex = null; + let threadIndex = null; + let indexInFlowMarkers = null; + + const markerContainerWidth = containerWidth - marginLeft - marginRight; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + + // Reminder: this is a value between 0 and 1, and represents a percentage of + // the full time range. + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + + // This is the x position in terms of unit interval (so, between 0 and 1). + const xInUnitInterval: UnitIntervalOfProfileRange = + viewportLeft + viewportLength * ((x - marginLeft) / markerContainerWidth); + + const dotRadiusInTime = + (dotRadius / markerContainerWidth) * viewportLength * rangeLength; + + const xInTime: Milliseconds = rangeStart + xInUnitInterval * rangeLength; + const rowIndex = Math.floor((y + viewportTop) / rowHeight); + + if (rowIndex < 0 || rowIndex >= flowTiming.rows.length) { + return null; + } + + const row = flowTiming.rows[rowIndex]; + const flowIndex = row.flowIndex; + const markerTiming = row.markers; + + if ( + !markerTiming || + typeof markerTiming === 'string' || + !markerTiming.length + ) { + return null; + } + + // This is a small utility function to define if some marker timing is in + // our hit test range. + const isMarkerTimingInDotRadius = (index) => + markerTiming.startTime[index] < xInTime + dotRadiusInTime && + markerTiming.endTime[index] > xInTime - dotRadiusInTime; + + // A markerTiming line is ordered. + // 1. Let's find a marker reasonably close to our mouse cursor. + // The result of this bisection gives the first marker that starts _after_ + // our mouse cursor. Our result will be either this marker, or the previous + // one. + const nextStartIndex = bisectionRight(markerTiming.startTime, xInTime); + + if (nextStartIndex > 0 && nextStartIndex < markerTiming.length) { + // 2. This is the common case: 2 markers are candidates. Then we measure + // the distance between them and the mouse cursor and chose the smallest + // distance. + const prevStartIndex = nextStartIndex - 1; + + // Note that these values can be negative if the cursor is _inside_ a + // marker. There should be one at most in this case, and we'll want it. So + // NO Math.abs here. + const distanceToNext = markerTiming.startTime[nextStartIndex] - xInTime; + const distanceToPrev = xInTime - markerTiming.endTime[prevStartIndex]; + + const closest = + distanceToPrev < distanceToNext ? prevStartIndex : nextStartIndex; + + // 3. When we found the closest, we still have to check if it's in close + // enough! + if (isMarkerTimingInDotRadius(closest)) { + markerIndex = markerTiming.markerIndex[closest]; + threadIndex = markerTiming.threadIndex[closest]; + indexInFlowMarkers = closest; + } + } else if (nextStartIndex === 0) { + // 4. Special case 1: the mouse cursor is at the left of all markers in + // this line. Then, we have only 1 candidate, we can check if it's inside + // our hit test range right away. + if (isMarkerTimingInDotRadius(nextStartIndex)) { + markerIndex = markerTiming.markerIndex[nextStartIndex]; + threadIndex = markerTiming.threadIndex[nextStartIndex]; + indexInFlowMarkers = nextStartIndex; + } + } else { + // 5. Special case 2: the mouse cursor is at the right of all markers in + // this line. Then we only have 1 candidate as well, let's check if it's + // inside our hit test range. + if (isMarkerTimingInDotRadius(nextStartIndex - 1)) { + markerIndex = markerTiming.markerIndex[nextStartIndex - 1]; + threadIndex = markerTiming.threadIndex[nextStartIndex - 1]; + indexInFlowMarkers = nextStartIndex - 1; + } + } + + return { + rowIndex, + flowIndex, + indexInFlowMarkers, + markerIndex, + threadIndex, + }; + }; + + onMouseMove = (event: { nativeEvent: MouseEvent }) => { + const { + changeMouseTimePosition, + rangeStart, + rangeEnd, + marginLeft, + marginRight, + viewport: { viewportLeft, viewportRight, containerWidth }, + } = this.props; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + const markerContainerWidth = containerWidth - marginLeft - marginRight; + // This is the x position in terms of unit interval (so, between 0 and 1). + const xInUnitInterval: UnitIntervalOfProfileRange = + viewportLeft + + viewportLength * + ((event.nativeEvent.offsetX - marginLeft) / markerContainerWidth); + + if (xInUnitInterval < 0 || xInUnitInterval > 1) { + changeMouseTimePosition(null); + } else { + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const xInTime: Milliseconds = rangeStart + xInUnitInterval * rangeLength; + changeMouseTimePosition(xInTime); + } + }; + + onMouseLeave = () => { + this.props.changeMouseTimePosition(null); + }; + + onDoubleClickMarker = (hoveredItems: HoveredFlowPanelItems | null) => { + const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; + const threadIndex = hoveredItems === null ? null : hoveredItems.threadIndex; + if (markerIndex === null || threadIndex === null) { + return; + } + const { + fullMarkerListPerThread, + updatePreviewSelection, + rangeStart, + rangeEnd, + } = this.props; + const marker = ensureExists( + fullMarkerListPerThread[threadIndex][markerIndex] + ); + const { start, end } = getStartEndRangeForMarker( + rangeStart, + rangeEnd, + marker + ); + + updatePreviewSelection({ + hasSelection: true, + isModifying: false, + selectionStart: start, + selectionEnd: end, + }); + }; + + onSelectItem = (hoveredItems: HoveredFlowPanelItems | null) => { + const flowIndex = hoveredItems === null ? null : hoveredItems.flowIndex; + if (flowIndex === null) { + return; + } + + const { changeActiveFlows } = this.props; + changeActiveFlows([flowIndex]); + }; + + onRightClickMarker = (_hoveredItems: HoveredFlowPanelItems | null) => { + // const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; + // const { changeRightClickedMarker, threadsKey } = this.props; + // changeRightClickedMarker(threadsKey, markerIndex); + }; + + getHoveredMarkerInfo = ({ + threadIndex, + markerIndex, + }: HoveredFlowPanelItems): React.Node => { + if ( + !this.props.shouldDisplayTooltips() || + threadIndex === null || + markerIndex === null + ) { + return null; + } + + const marker = ensureExists( + this.props.fullMarkerListPerThread[threadIndex][markerIndex] + ); + return ( + + ); + }; + + render() { + const { containerWidth, containerHeight, isDragging } = this.props.viewport; + + return ( + + ); + } +} + +export const FlowPanelCanvas = (withChartViewport: WithChartViewport< + OwnProps, + Props, +>)(FlowPanelCanvasImpl); diff --git a/src/components/flow-panel/index.css b/src/components/flow-panel/index.css new file mode 100644 index 0000000000..7451d9d019 --- /dev/null +++ b/src/components/flow-panel/index.css @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.flowPanel { + display: flex; + flex: 1; + flex-flow: column nowrap; +} + +.markerChartCanvas { + border-top: 1px solid var(--grey-30); +} diff --git a/src/components/flow-panel/index.js b/src/components/flow-panel/index.js new file mode 100644 index 0000000000..c5bc3ae852 --- /dev/null +++ b/src/components/flow-panel/index.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow +import * as React from 'react'; +import { TIMELINE_MARGIN_RIGHT } from 'firefox-profiler/app-logic/constants'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { FlowPanelCanvas } from './Canvas'; + +import { + getCommittedRange, + getPreviewSelection, +} from 'firefox-profiler/selectors/profile'; +import { + getFullMarkerListPerThread, + getMarkerChartLabelGetterPerThread, + getFlowTiming, +} from 'firefox-profiler/selectors/flow'; +import { getTimelineMarginLeft } from 'firefox-profiler/selectors/app'; +import { + updatePreviewSelection, + changeMouseTimePosition, + changeActiveFlows, +} from 'firefox-profiler/actions/profile-view'; +import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; + +import type { + Marker, + MarkerIndex, + FlowTiming, + UnitIntervalOfProfileRange, + StartEndRange, + PreviewSelection, + CssPixels, +} from 'firefox-profiler/types'; + +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './index.css'; + +const ROW_HEIGHT = 16; + +type DispatchProps = {| + +updatePreviewSelection: typeof updatePreviewSelection, + +changeMouseTimePosition: typeof changeMouseTimePosition, + +changeActiveFlows: typeof changeActiveFlows, +|}; + +type StateProps = {| + +fullMarkerListPerThread: Marker[][], + +markerLabelGetterPerThread: Array<(MarkerIndex) => string>, + +flowTiming: FlowTiming, + +timeRange: StartEndRange, + +previewSelection: PreviewSelection, + +timelineMarginLeft: CssPixels, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +class FlowPanelImpl extends React.PureComponent { + _viewport: HTMLDivElement | null = null; + + /** + * Determine the maximum zoom of the viewport. + */ + getMaximumZoom(): UnitIntervalOfProfileRange { + const { + timeRange: { start, end }, + } = this.props; + + // This is set to a very small value, that represents 1ns. We can't set it + // to zero unless we revamp how ranges are handled in the app to prevent + // less-than-1ns ranges, otherwise we can get stuck at a "0" zoom. + const ONE_NS = 1e-6; + return ONE_NS / (end - start); + } + + _shouldDisplayTooltips = () => true; + + _takeViewportRef = (viewport: HTMLDivElement | null) => { + this._viewport = viewport; + }; + + _focusViewport = () => { + if (this._viewport) { + this._viewport.focus(); + } + }; + + componentDidMount() { + this._focusViewport(); + } + + render() { + const { + timeRange, + flowTiming, + fullMarkerListPerThread, + markerLabelGetterPerThread, + previewSelection, + updatePreviewSelection, + changeMouseTimePosition, + changeActiveFlows, + timelineMarginLeft, + } = this.props; + + // The viewport needs to know about the height of what it's drawing, calculate + // that here at the top level component. + const rowCount = flowTiming.rows.length; + const maxViewportHeight = rowCount * ROW_HEIGHT; + + return ( +
+ {rowCount === 0 ? null : ( + + + + )} +
+ ); + } +} + +// This function is given the FlowPanelCanvas's chartProps. +function viewportNeedsUpdate( + prevProps: { +flowTiming: FlowTiming }, + newProps: { +flowTiming: FlowTiming } +) { + return prevProps.flowTiming !== newProps.flowTiming; +} + +export const FlowPanel = explicitConnect<{||}, StateProps, DispatchProps>({ + mapStateToProps: (state) => { + const flowTiming = getFlowTiming(state); + return { + fullMarkerListPerThread: getFullMarkerListPerThread(state), + markerLabelGetterPerThread: getMarkerChartLabelGetterPerThread(state), + flowTiming, + timeRange: getCommittedRange(state), + previewSelection: getPreviewSelection(state), + timelineMarginLeft: getTimelineMarginLeft(state), + }; + }, + mapDispatchToProps: { + updatePreviewSelection, + changeMouseTimePosition, + changeActiveFlows, + }, + component: FlowPanelImpl, +}); diff --git a/src/components/marker-chart-tab/index.css b/src/components/marker-chart-tab/index.css new file mode 100644 index 0000000000..062ddd03cb --- /dev/null +++ b/src/components/marker-chart-tab/index.css @@ -0,0 +1,9 @@ +.markerChartTabContainer { + position: relative; + min-height: 0; + flex: 1; +} + +.markerChartTabSplitter > .layout-pane { + display: flex; +} diff --git a/src/components/marker-chart-tab/index.js b/src/components/marker-chart-tab/index.js new file mode 100644 index 0000000000..f4561de97c --- /dev/null +++ b/src/components/marker-chart-tab/index.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow +import * as React from 'react'; + +import SplitterLayout from 'react-splitter-layout'; +import { MarkerChart } from '../marker-chart'; +import { FlowPanel } from '../flow-panel'; + +import './index.css'; + +export function MarkerChartTab() { + return ( +
+ + + + +
+ ); +} diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index 8ecf638e07..7a286f86b9 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -20,6 +20,7 @@ import { typeof changeRightClickedMarker as ChangeRightClickedMarker, typeof changeMouseTimePosition as ChangeMouseTimePosition, typeof changeSelectedMarker as ChangeSelectedMarker, + typeof activateFlowsForMarker as ActivateFlowsForMarker, } from 'firefox-profiler/actions/profile-view'; import { TIMELINE_MARGIN_LEFT } from 'firefox-profiler/app-logic/constants'; import type { @@ -87,6 +88,7 @@ type OwnProps = {| +changeMouseTimePosition: ChangeMouseTimePosition, +changeSelectedMarker: ChangeSelectedMarker, +changeRightClickedMarker: ChangeRightClickedMarker, + +activateFlowsForMarker: WrapFunctionInDispatch, +marginLeft: CssPixels, +marginRight: CssPixels, +selectedMarkerIndex: MarkerIndex | null, @@ -893,8 +895,15 @@ class MarkerChartCanvasImpl extends React.PureComponent { onSelectItem = (hoveredItems: HoveredMarkerChartItems | null) => { const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; - const { changeSelectedMarker, threadsKey } = this.props; + const { changeSelectedMarker, activateFlowsForMarker, threadsKey } = + this.props; changeSelectedMarker(threadsKey, markerIndex, { source: 'pointer' }); + console.log({ threadsKey, markerIndex }); + if (typeof threadsKey === 'number' && markerIndex !== null) { + console.log('hello'); + const what = activateFlowsForMarker(threadsKey, markerIndex); + console.log({ what }); + } }; onRightClickMarker = (hoveredItems: HoveredMarkerChartItems | null) => { diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index 17abbb99cf..f6779774c8 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -25,6 +25,7 @@ import { changeRightClickedMarker, changeMouseTimePosition, changeSelectedMarker, + activateFlowsForMarker, } from 'firefox-profiler/actions/profile-view'; import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; @@ -51,6 +52,7 @@ type DispatchProps = {| +changeRightClickedMarker: typeof changeRightClickedMarker, +changeMouseTimePosition: typeof changeMouseTimePosition, +changeSelectedMarker: typeof changeSelectedMarker, + +activateFlowsForMarker: typeof activateFlowsForMarker, |}; type StateProps = {| @@ -117,6 +119,7 @@ class MarkerChartImpl extends React.PureComponent { updatePreviewSelection, changeMouseTimePosition, changeRightClickedMarker, + activateFlowsForMarker, rightClickedMarkerIndex, selectedMarkerIndex, changeSelectedMarker, @@ -129,12 +132,7 @@ class MarkerChartImpl extends React.PureComponent { const maxViewportHeight = maxMarkerRows * ROW_HEIGHT; return ( -
+
{maxMarkerRows === 0 ? ( @@ -166,6 +164,8 @@ class MarkerChartImpl extends React.PureComponent { updatePreviewSelection, changeMouseTimePosition, changeRightClickedMarker, + // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. + activateFlowsForMarker, rangeStart: timeRange.start, rangeEnd: timeRange.end, rowHeight: ROW_HEIGHT, @@ -220,6 +220,7 @@ export const MarkerChart = explicitConnect<{||}, StateProps, DispatchProps>({ changeMouseTimePosition, changeRightClickedMarker, changeSelectedMarker, + activateFlowsForMarker, }, component: MarkerChartImpl, }); diff --git a/src/profile-logic/marker-data.js b/src/profile-logic/marker-data.js index aaf4c6c571..ca2e300a8a 100644 --- a/src/profile-logic/marker-data.js +++ b/src/profile-logic/marker-data.js @@ -45,6 +45,15 @@ import type { MarkerDisplayLocation, Tid, Milliseconds, + FlowMarker, + FlowSchemasByName, + ProfileFlowInfo, + FlowTiming, + IndexIntoFlowTable, + ConnectedFlowInfo, + FlowTimingRow, + FlowTimingRowType, + FlowTimingRowMarkerTable, } from 'firefox-profiler/types'; import type { UniqueStringArray } from '../utils/unique-string-array'; @@ -1640,58 +1649,6 @@ export const stringsToMarkerRegExps = ( }; }; -export type GlobalFlowMarkerHandle = {| - threadIndex: number, - flowMarkerIndex: number, -|}; - -// An index into the global flow table. -type IndexIntoFlowTable = number; - -export type Flow = {| - id: string, - startTime: Milliseconds, - endTime: Milliseconds | null, - // All markers which mention this flow, ordered by start time. - flowMarkers: GlobalFlowMarkerHandle[], -|}; - -export type ConnectedFlowInfo = {| - // Flows whose marker set has a non-empty intersection with our marker set. - directlyConnectedFlows: IndexIntoFlowTable[], - // Flows which have at least one marker in their marker set was a stack-based - // marker which was already running higher up on the stack when at least one - // of our stack-based or instant markers was running on the same thread. - // All flows in our incomingContextFlows set have this flow in their - // outgoingContextFlows set. - incomingContextFlows: IndexIntoFlowTable[], - // Flows which have at least one stack-based or instant marker in their marker - // set which was running when one of the stack-based markers in our set was - // running higher up on the same thread's stack. - // All flows in our outgoingContextFlows set have this flow in their - // incomingContextFlows set. - outgoingContextFlows: IndexIntoFlowTable[], -|}; - -type FlowIDAndTerminating = {| - flowID: string, - isTerminating: boolean, -|}; - -export type FlowMarker = {| - markerIndex: number, - time: Milliseconds, - // The index of the closest stack-based interval flow marker that encompasses - // this marker. ("Closest" means "with the most recent start time".) - // If non-null, parentContextFlowMarker is lower the index of this flow marker, - // i.e. this can only point "backwards" within the thread's flow markers array. - parentContextFlowMarker: number | null, - // The indexes of flow markers which have this flow marker as their parentContextFlowMarker. - // All indexes in this array after the index of this flow marker. - childContextFlowMarkers: number[], - flowIDs: FlowIDAndTerminating[], -|}; - class MinHeap { _keys: number[] = []; _values: V[] = []; @@ -1731,18 +1688,6 @@ class MinHeap { } } -export type FlowFieldDescriptor = {| - key: string, - isTerminating: boolean, -|}; - -export type FlowSchema = {| - flowFields: FlowFieldDescriptor[], - isStackBased: boolean, -|}; - -export type FlowSchemasByName = Map; - export function computeFlowSchemasByName( markerSchemas: MarkerSchema[] ): FlowSchemasByName { @@ -1797,11 +1742,12 @@ export function computeFlowMarkers( continue; } - const time = marker.start; + const startTime = marker.start; + const endTime = marker.end; while ( currentContextEndTimes.length !== 0 && - currentContextEndTimes[currentContextEndTimes.length - 1] < time + currentContextEndTimes[currentContextEndTimes.length - 1] < startTime ) { currentContextEndTimes.pop(); currentContextFlowMarkers.pop(); @@ -1828,7 +1774,8 @@ export function computeFlowMarkers( parentContextFlowMarker, childContextFlowMarkers: [], flowIDs, - time, + startTime, + endTime, markerIndex, }); if (flowSchema.isStackBased || marker.end === null) { @@ -1846,14 +1793,6 @@ export function computeFlowMarkers( return flowMarkers; } -export type ProfileFlowInfo = {| - flowTable: Flow[], - flowsByID: Map, - flowMarkersPerThread: FlowMarker[][], - flowMarkerFlowsPerThread: IndexIntoFlowTable[][][], - flowSchemasByName: FlowSchemasByName, -|}; - export function computeProfileFlowInfo( fullMarkerListPerThread: Marker[][], threads: Thread[], @@ -1873,7 +1812,7 @@ export function computeProfileFlowInfo( for (let threadIndex = 0; threadIndex < threadCount; threadIndex++) { const flowMarkers = flowMarkersPerThread[threadIndex]; if (flowMarkers.length !== 0) { - nextEntryHeap.insert(flowMarkers[0].time, { + nextEntryHeap.insert(flowMarkers[0].startTime, { threadIndex, nextIndex: 0, }); @@ -1896,9 +1835,10 @@ export function computeProfileFlowInfo( const { threadIndex, nextIndex } = nextEntry; const flowMarkerIndex = nextIndex; const flowMarkers = flowMarkersPerThread[threadIndex]; - const flowReference = flowMarkers[nextIndex]; + const flowMarker = flowMarkers[nextIndex]; - const { flowIDs, time } = flowReference; + const { markerIndex, flowIDs } = flowMarker; + const { start, end } = fullMarkerListPerThread[threadIndex][markerIndex]; const flowMarkerHandle = { threadIndex, flowMarkerIndex }; const flowsForThisFlowMarker = []; for (const { flowID, isTerminating } of flowIDs) { @@ -1907,8 +1847,8 @@ export function computeProfileFlowInfo( flowIndex = flowTable.length; flowTable.push({ id: flowID, - startTime: time, - endTime: time, + startTime: start, + endTime: end ?? start, flowMarkers: [flowMarkerHandle], }); if (!isTerminating) { @@ -1923,7 +1863,7 @@ export function computeProfileFlowInfo( } else { const flow = flowTable[flowIndex]; flow.flowMarkers.push(flowMarkerHandle); - flow.endTime = time; + flow.endTime = end ?? start; if (isTerminating) { currentActiveFlows.delete(flowID); } @@ -1937,7 +1877,7 @@ export function computeProfileFlowInfo( const newNextIndex = nextIndex + 1; if (newNextIndex < flowMarkers.length) { nextEntry.nextIndex = newNextIndex; - nextEntryHeap.reorder(handle, flowMarkers[newNextIndex].time); + nextEntryHeap.reorder(handle, flowMarkers[newNextIndex].startTime); } else { nextEntryHeap.delete(handle); } @@ -2028,11 +1968,11 @@ export function lookupFlow( return candidateFlows[index]; } -export function computeMarkerFlowsForConsole( +export function computeMarkerFlows( threadIndex: number, markerIndex: MarkerIndex, profileFlowInfo: ProfileFlowInfo, - threads: Thread[], + stringTablePerThread: UniqueStringArray[], fullMarkerListPerThread: Marker[][] ): IndexIntoFlowTable[] | null { const marker = fullMarkerListPerThread[threadIndex][markerIndex]; @@ -2049,7 +1989,7 @@ export function computeMarkerFlowsForConsole( return null; } - const stringTable = threads[threadIndex].stringTable; + const stringTable = stringTablePerThread[threadIndex]; const flowIndexes = []; for (const { key } of flowSchema.flowFields) { @@ -2098,13 +2038,14 @@ export function printMarkerFlows( markerIndex: MarkerIndex, profileFlowInfo: ProfileFlowInfo, threads: Thread[], + stringTablePerThread: UniqueStringArray[], fullMarkerListPerThread: Marker[][] ) { - const markerFlows = computeMarkerFlowsForConsole( + const markerFlows = computeMarkerFlows( markerThreadIndex, markerIndex, profileFlowInfo, - threads, + stringTablePerThread, fullMarkerListPerThread ); if (markerFlows === null) { @@ -2149,7 +2090,7 @@ export function printFlow( const thread = threads[threadIndex]; const marker = fullMarkerListPerThread[threadIndex][otherMarkerIndex]; console.log( - ` - marker ${otherMarkerIndex} (thread index: ${threadIndex}) at time ${flowMarker.time} on thread ${thread.name} (pid: ${thread.pid}, tid: ${thread.tid}):`, + ` - marker ${otherMarkerIndex} (thread index: ${threadIndex}) at time ${flowMarker.startTime} on thread ${thread.name} (pid: ${thread.pid}, tid: ${thread.tid}):`, marker ); const directlyConnectedFlows = flowMarkerFlowsPerThread[threadIndex][ @@ -2206,3 +2147,95 @@ export function printFlow( // ); // } } + +export function computeFlowTiming( + profileFlowInfo: ProfileFlowInfo, + activeFlows: IndexIntoFlowTable[] +): FlowTiming { + let incomingContextFlows = []; + const mainFlows = activeFlows.slice(); + let outgoingContextFlows = []; + + for (const flow of activeFlows) { + const connectedFlows = getConnectedFlowInfo(flow, profileFlowInfo); + incomingContextFlows.push(...connectedFlows.incomingContextFlows); + mainFlows.push(...connectedFlows.directlyConnectedFlows); + outgoingContextFlows.push(...connectedFlows.outgoingContextFlows); + } + sortAndDedup(incomingContextFlows); + sortAndDedup(mainFlows); + sortAndDedup(outgoingContextFlows); + + incomingContextFlows = incomingContextFlows.filter( + (icf) => mainFlows.indexOf(icf) === -1 + ); + outgoingContextFlows = outgoingContextFlows.filter( + (icf) => + incomingContextFlows.indexOf(icf) === -1 && mainFlows.indexOf(icf) === -1 + ); + + const rawRows: Array<[FlowTimingRowType, IndexIntoFlowTable]> = [ + ...incomingContextFlows.map((flowIndex) => ['INCOMING_CONTEXT', flowIndex]), + ...mainFlows.map((flowIndex) => ['ACTIVE', flowIndex]), + ...outgoingContextFlows.map((flowIndex) => ['OUTGOING_CONTEXT', flowIndex]), + ]; + + const arrowTable = { + length: 0, + time: [], + threadFrom: [], + threadTo: [], + markerIndexFrom: [], + markerIndexTo: [], + rowIndexFrom: [], + rowIndexTo: [], + isDirected: [], + }; + + const rows: FlowTimingRow[] = []; + + const { flowTable, flowMarkersPerThread } = profileFlowInfo; + + for (const [rowType, flowIndex] of rawRows) { + const flow = flowTable[flowIndex]; + const { flowMarkers, startTime, endTime } = flow; + const flowMarkerCount = flowMarkers.length; + + const markers: FlowTimingRowMarkerTable = { + length: flowMarkerCount, + threadIndex: new Int32Array(flowMarkerCount), + markerIndex: new Int32Array(flowMarkerCount), + startTime: new Float64Array(flowMarkerCount), + endTime: new Float64Array(flowMarkerCount), + isInstant: new Uint8Array(flowMarkerCount), + arrowIndexes: [], + }; + + for (let i = 0; i < flowMarkerCount; i++) { + const { threadIndex, flowMarkerIndex } = flowMarkers[i]; + const flowMarker = flowMarkersPerThread[threadIndex][flowMarkerIndex]; + const { markerIndex, startTime, endTime } = flowMarker; + markers.threadIndex[i] = threadIndex; + markers.markerIndex[i] = markerIndex; + markers.startTime[i] = startTime; + if (endTime === null) { + markers.endTime[i] = startTime; + markers.isInstant[i] = 1; + } else { + markers.endTime[i] = endTime; + } + markers.arrowIndexes[i] = []; + } + + rows.push({ + label: `Flow ${flowIndex}`, + rowType, + flowIndex, + flowStart: startTime, + flowEnd: ensureExists(endTime), + markers, + }); + } + + return { rows, arrowTable }; +} diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index 1d66a4a006..7ca8f81358 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -22,6 +22,7 @@ import type { Reducer, TimelineTrackOrganization, SourceViewState, + IndexIntoFlowTable, AssemblyViewState, IsOpenPerPanelState, TabID, @@ -658,6 +659,17 @@ const isBottomBoxOpenPerPanel: Reducer = ( } }; +const activeFlows: Reducer = (state = [], action) => { + switch (action.type) { + case 'CHANGE_ACTIVE_FLOWS': { + const { activeFlows } = action; + return activeFlows; + } + default: + return state; + } +}; + /** * Active tab specific profile url states */ @@ -723,6 +735,7 @@ const profileSpecific = combineReducers({ transforms, sourceView, assemblyView, + activeFlows, isBottomBoxOpenPerPanel, timelineType, full: fullProfileSpecific, diff --git a/src/selectors/flow.js b/src/selectors/flow.js index f72273679f..34d9a9af86 100644 --- a/src/selectors/flow.js +++ b/src/selectors/flow.js @@ -5,13 +5,24 @@ // @flow import { createSelector } from 'reselect'; -import { computeProfileFlowInfo } from '../profile-logic/marker-data'; -import type { ProfileFlowInfo } from '../profile-logic/marker-data'; +import { + computeProfileFlowInfo, + computeFlowTiming, +} from '../profile-logic/marker-data'; import { getThreadSelectors } from './per-thread'; +import { getActiveFlows } from './url-state'; import type { ThreadSelectors } from './per-thread'; import { getThreads, getMarkerSchema } from './profile'; -import type { Selector, State, Marker } from 'firefox-profiler/types'; +import type { + Selector, + State, + MarkerIndex, + Marker, + ProfileFlowInfo, + FlowTiming, +} from 'firefox-profiler/types'; +import { UniqueStringArray } from 'firefox-profiler/utils/unique-string-array'; function _arraysShallowEqual(arr1: any[], arr2: any[]): boolean { return arr1.length === arr2.length && arr1.every((val, i) => val === arr2[i]); @@ -40,9 +51,26 @@ export const getFullMarkerListPerThread: Selector = getFullMarkerList(state) ); +export const getMarkerChartLabelGetterPerThread: Selector< + Array<(MarkerIndex) => string>, +> = _createSelectorForAllThreads(({ getMarkerChartLabelGetter }, state) => + getMarkerChartLabelGetter(state) +); + +export const getStringTablePerThread: Selector = + _createSelectorForAllThreads(({ getStringTable }, state) => + getStringTable(state) + ); + export const getProfileFlowInfo: Selector = createSelector( getFullMarkerListPerThread, getThreads, getMarkerSchema, computeProfileFlowInfo ); + +export const getFlowTiming: Selector = createSelector( + getProfileFlowInfo, + getActiveFlows, + computeFlowTiming +); diff --git a/src/selectors/url-state.js b/src/selectors/url-state.js index 392b271ef3..2d805f6be1 100644 --- a/src/selectors/url-state.js +++ b/src/selectors/url-state.js @@ -35,6 +35,7 @@ import type { ActiveTabSpecificProfileUrlState, NativeSymbolInfo, TabID, + IndexIntoFlowTable, } from 'firefox-profiler/types'; import type { TabSlug } from '../app-logic/tabs-handling'; @@ -112,6 +113,8 @@ export const getNetworkSearchString: Selector = (state) => getProfileSpecificState(state).networkSearchString; export const getSelectedTab: Selector = (state) => getUrlState(state).selectedTab; +export const getActiveFlows: Selector = (state) => + getProfileSpecificState(state).activeFlows; export const getInvertCallstack: Selector = (state) => getSelectedTab(state) === 'calltree' && getProfileSpecificState(state).invertCallstack; diff --git a/src/types/actions.js b/src/types/actions.js index 99ca82a98f..3ef5c50c74 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -26,6 +26,7 @@ import type { ActiveTabTimeline, ThreadsKey, NativeSymbolInfo, + IndexIntoFlowTable, } from './profile-derived'; import type { FuncToFuncsMap } from '../profile-logic/symbolication'; import type { TemporaryError } from '../utils/errors'; @@ -248,6 +249,10 @@ type ProfileAction = +threadsKey: ThreadsKey, +markerIndex: MarkerIndex | null, |} + | {| + +type: 'CHANGE_ACTIVE_FLOWS', + +activeFlows: IndexIntoFlowTable[], + |} | {| +type: 'UPDATE_PREVIEW_SELECTION', +previewSelection: PreviewSelection, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index c06774e777..c2d8915d8f 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -391,6 +391,120 @@ export type ThreadWithReservedFunctions = {| >, |}; +export type GlobalFlowMarkerHandle = {| + threadIndex: number, + flowMarkerIndex: number, +|}; + +// An index into the global flow table. +export type IndexIntoFlowTable = number; + +export type Flow = {| + id: string, + startTime: Milliseconds, + endTime: Milliseconds | null, + // All markers which mention this flow, ordered by start time. + flowMarkers: GlobalFlowMarkerHandle[], +|}; + +export type ConnectedFlowInfo = {| + // Flows whose marker set has a non-empty intersection with our marker set. + directlyConnectedFlows: IndexIntoFlowTable[], + // Flows which have at least one marker in their marker set was a stack-based + // marker which was already running higher up on the stack when at least one + // of our stack-based or instant markers was running on the same thread. + // All flows in our incomingContextFlows set have this flow in their + // outgoingContextFlows set. + incomingContextFlows: IndexIntoFlowTable[], + // Flows which have at least one stack-based or instant marker in their marker + // set which was running when one of the stack-based markers in our set was + // running higher up on the same thread's stack. + // All flows in our outgoingContextFlows set have this flow in their + // incomingContextFlows set. + outgoingContextFlows: IndexIntoFlowTable[], +|}; + +type FlowIDAndTerminating = {| + flowID: string, + isTerminating: boolean, +|}; + +export type FlowMarker = {| + markerIndex: number, + startTime: Milliseconds, + endTime: Milliseconds | null, + // The index of the closest stack-based interval flow marker that encompasses + // this marker. ("Closest" means "with the most recent start time".) + // If non-null, parentContextFlowMarker is lower the index of this flow marker, + // i.e. this can only point "backwards" within the thread's flow markers array. + parentContextFlowMarker: number | null, + // The indexes of flow markers which have this flow marker as their parentContextFlowMarker. + // All indexes in this array after the index of this flow marker. + childContextFlowMarkers: number[], + flowIDs: FlowIDAndTerminating[], +|}; + +export type FlowFieldDescriptor = {| + key: string, + isTerminating: boolean, +|}; + +export type FlowSchema = {| + flowFields: FlowFieldDescriptor[], + isStackBased: boolean, +|}; + +export type FlowSchemasByName = Map; + +export type ProfileFlowInfo = {| + flowTable: Flow[], + flowsByID: Map, + flowMarkersPerThread: FlowMarker[][], + flowMarkerFlowsPerThread: IndexIntoFlowTable[][][], + flowSchemasByName: FlowSchemasByName, +|}; + +export type FlowTiming = {| + rows: FlowTimingRow[], + arrowTable: FlowTimingArrowTable, +|}; + +export type FlowTimingRowType = + | 'INCOMING_CONTEXT' + | 'ACTIVE' + | 'OUTGOING_CONTEXT'; + +export type FlowTimingRow = {| + rowType: FlowTimingRowType, + label: string, + flowIndex: IndexIntoFlowTable, + flowStart: Milliseconds, + flowEnd: Milliseconds, + markers: FlowTimingRowMarkerTable, +|}; + +export type FlowTimingRowMarkerTable = {| + length: number, + threadIndex: Int32Array, // ThreadIndex[], + markerIndex: Int32Array, // MarkerIndex[], + startTime: Float64Array, // Milliseconds[], + endTime: Float64Array, // Milliseconds[], + isInstant: Uint8Array, // boolean[], + arrowIndexes: number[][], +|}; + +export type FlowTimingArrowTable = {| + length: number, + time: Milliseconds[], + threadFrom: ThreadIndex[], + threadTo: ThreadIndex[], + markerIndexFrom: MarkerIndex[], + markerIndexTo: MarkerIndex[], + rowIndexFrom: number[], + rowIndexTo: number[], + isDirected: boolean[], +|}; + /** * The marker timing contains the necessary information to draw markers very quickly * in the marker chart. It represents a single row of markers in the chart. diff --git a/src/types/state.js b/src/types/state.js index 332a5459ac..47b0b16a36 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -33,6 +33,7 @@ import type { LocalTrack, TrackIndex, MarkerIndex, + IndexIntoFlowTable, ActiveTabTimeline, OriginsTimeline, ThreadsKey, @@ -385,6 +386,7 @@ export type ProfileSpecificUrlState = {| isBottomBoxOpenPerPanel: IsOpenPerPanelState, full: FullProfileSpecificUrlState, activeTab: ActiveTabSpecificProfileUrlState, + activeFlows: IndexIntoFlowTable[], |}; /** diff --git a/src/utils/window-console.js b/src/utils/window-console.js index 3f057d3938..cf25c416e4 100644 --- a/src/utils/window-console.js +++ b/src/utils/window-console.js @@ -80,6 +80,8 @@ export function addDataToWindowObject( const profileFlowInfo = selectorsForConsole.flow.getProfileFlowInfo(getState()); const threads = selectorsForConsole.profile.getThreads(getState()); + const stringTablePerThread = + selectorsForConsole.flow.getStringTablePerThread(getState()); const fullMarkerListPerThread = selectorsForConsole.flow.getFullMarkerListPerThread(getState()); if (markerIndex === null) { @@ -90,6 +92,7 @@ export function addDataToWindowObject( markerIndex, profileFlowInfo, threads, + stringTablePerThread, fullMarkerListPerThread ); }