diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index a7bf7ddc39..7950ec31a1 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -347,10 +347,7 @@ function getInformationFromTrackReference( relatedThreadIndex: localTrack.threadIndex, relatedTab: null, }; - case 'memory': - case 'bandwidth': - case 'process-cpu': - case 'power': { + case 'counter': { const counterSelectors = getCounterSelectors(localTrack.counterIndex); const counter = counterSelectors.getCounter(state); return { diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index 955a410194..a8bdb0e23c 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -29,16 +29,10 @@ export const TRACK_NETWORK_ROW_REPEAT = 7; export const TRACK_NETWORK_HEIGHT = TRACK_NETWORK_ROW_HEIGHT * TRACK_NETWORK_ROW_REPEAT; -// The following values are for memory track. -export const TRACK_MEMORY_GRAPH_HEIGHT = 25; -export const TRACK_MEMORY_MARKERS_HEIGHT = 15; -export const TRACK_MEMORY_HEIGHT = - TRACK_MEMORY_GRAPH_HEIGHT + TRACK_MEMORY_MARKERS_HEIGHT; -export const TRACK_MEMORY_LINE_WIDTH = 2; - -// The following values are for the bandwidth track. -export const TRACK_BANDWIDTH_HEIGHT = 25; -export const TRACK_BANDWIDTH_LINE_WIDTH = 2; +// The following values are for counter tracks (Memory, Power, Bandwidth, etc.). +export const TRACK_COUNTER_GRAPH_HEIGHT = 25; +export const TRACK_COUNTER_MARKERS_HEIGHT = 15; +export const TRACK_COUNTER_LINE_WIDTH = 2; // The following values are for experimental event delay track. export const TRACK_EVENT_DELAY_HEIGHT = 40; @@ -59,14 +53,6 @@ export const TRACK_PROCESS_BLANK_HEIGHT = 30; // Height of timeline ruler. export const TIMELINE_RULER_HEIGHT = 20; -// Height of the power track. -export const TRACK_POWER_HEIGHT = 25; -export const TRACK_POWER_LINE_WIDTH = 2; - -// Height of the process cpu track. -export const TRACK_PROCESS_CPU_HEIGHT = 25; -export const TRACK_PROCESS_CPU_LINE_WIDTH = 2; - // JS Tracer has very high fidelity information, and needs a more fine-grained zoom. export const JS_TRACER_MAXIMUM_CHART_ZOOM = 0.001; diff --git a/src/components/timeline/LocalTrack.tsx b/src/components/timeline/LocalTrack.tsx index f43e2b991f..ff670bb722 100644 --- a/src/components/timeline/LocalTrack.tsx +++ b/src/components/timeline/LocalTrack.tsx @@ -26,11 +26,8 @@ import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { TimelineTrackThread } from './TrackThread'; import { TrackEventDelay } from './TrackEventDelay'; import { TrackNetwork } from './TrackNetwork'; -import { TrackMemory } from './TrackMemory'; -import { TrackBandwidth } from './TrackBandwidth'; +import { TrackCounter } from './TrackCounter'; import { TrackIPC } from './TrackIPC'; -import { TrackProcessCPU } from './TrackProcessCPU'; -import { TrackPower } from './TrackPower'; import { getTrackSelectionModifiers } from 'firefox-profiler/utils'; import type { TrackReference, @@ -106,18 +103,12 @@ class LocalTrackComponent extends PureComponent { ); case 'network': return ; - case 'memory': - return ; - case 'bandwidth': - return ; + case 'counter': + return ; case 'ipc': return ; case 'event-delay': return ; - case 'process-cpu': - return ; - case 'power': - return ; case 'marker': return ( ; - -type State = {}; - -export class TrackBandwidthImpl extends React.PureComponent { - override render() { - const { counterIndex } = this.props; - return ( -
- -
- ); - } -} - -export const TrackBandwidth = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - return { - threadIndex: counter.mainThreadIndex, - rangeStart: start, - rangeEnd: end, - }; - }, - component: TrackBandwidthImpl, -}); diff --git a/src/components/timeline/TrackBandwidthGraph.tsx b/src/components/timeline/TrackBandwidthGraph.tsx deleted file mode 100644 index 0110b3c9a3..0000000000 --- a/src/components/timeline/TrackBandwidthGraph.tsx +++ /dev/null @@ -1,705 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { InView } from 'react-intersection-observer'; -import { Localized } from '@fluent/react'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { - getStrokeColor, - getFillColor, - getDotColor, -} from 'firefox-profiler/profile-logic/graph-color'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - formatBytes, - formatNumber, -} from 'firefox-profiler/utils/format-numbers'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getPreviewSelection, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { - TooltipDetails, - TooltipDetail, - TooltipDetailSeparator, -} from 'firefox-profiler/components/tooltip/TooltipDetails'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; -import { co2 } from '@tgwf/co2'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - AccumulatedCounterSamples, - Milliseconds, - PreviewSelection, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackBandwidth.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackBandwidthCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _requestedAnimationFrame: boolean = false; - _canvasState: { renderScheduled: boolean; inView: boolean } = { - renderScheduled: false, - inView: false, - }; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - maxCounterSampleCountPerMs, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - // Take the sample information, and convert it into chart coordinates. Use a slightly - // smaller space than the deviceHeight, so that the stroke will be fully visible - // both at the top and bottom of the chart. - const [sampleStart, sampleEnd] = counterSampleRange; - const countRangePerMs = maxCounterSampleCountPerMs; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // bandwidth graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor(counter.display.color); - ctx.fillStyle = getFillColor(counter.display.color); - ctx.beginPath(); - - const getX = (i: number) => - Math.round((samples.time[i] - rangeStart) * millisecondWidth); - const getY = (i: number) => { - const rawY = samples.count[i]; - if (!rawY) { - // Make the 0 values invisible so that 'almost 0' is noticeable. - return deviceHeight + deviceLineHalfWidth; - } - - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - const unitGraphCount = rawY / sampleTimeDeltaInMs / countRangePerMs; - return ( - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - deviceLineHalfWidth - ); - }; - - // The x and y are used after the loop. - const firstX = getX(sampleStart); - let x = firstX; - let y = getY(sampleStart); - - // For the first sample, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - ctx.moveTo(x, y); - - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - for (let i = sampleStart + 1; i < sampleEnd; i++) { - x = getX(i); - y = getY(i); - ctx.lineTo(x, y); - - // If we have multiple samples to draw on the same horizontal pixel, - // we process all of them together with a max-min decimation algorithm - // to save time: - // - We draw the first and last samples to ensure the display is - // correct if there are sampling gaps. - // - For the values in between, we only draw the min and max values, - // to draw a vertical line covering all the other sample values. - const values = [y]; - while (i + 1 < sampleEnd && getX(i + 1) === x) { - values.push(getY(++i)); - } - - // Looking for the min and max only makes sense if we have more than 2 - // samples to draw. - if (values.length > 2) { - const maxY = Math.max(...values); - if (maxY !== y) { - y = maxY; - ctx.lineTo(x, y); - } - const minY = Math.min(...values); - if (minY !== y) { - y = minY; - ctx.lineTo(x, y); - } - } - - const lastY = values[values.length - 1]; - if (lastY !== y) { - y = lastY; - ctx.lineTo(x, y); - } - } - - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _scheduleDraw() { - if (!this._canvasState.inView) { - // Canvas is not in the view. Schedule the render for a later intersection - // observer callback. - this._canvasState.renderScheduled = true; - return; - } - - // Canvas is in the view. Render the canvas and reset the schedule state. - this._canvasState.renderScheduled = false; - - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - const canvas = this._canvas; - if (canvas) { - this.drawCanvas(canvas); - } - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { - this._canvasState.inView = inView; - if (!this._canvasState.renderScheduled) { - // Skip if render is not scheduled. - return; - } - - this._scheduleDraw(); - }; - - override componentDidMount() { - this._scheduleDraw(); - } - - override componentDidUpdate() { - this._scheduleDraw(); - } - - override render() { - return ( - - - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; - readonly previewSelection: PreviewSelection | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The bandwidth track graph takes bandwidth information from counters, and renders it as a - * graph in the timeline. - */ -class TrackBandwidthGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - // This persistTooltips property is part of the web console API. It helps - // in being able to inspect and debug tooltips. - if (window.persistTooltips) { - return; - } - - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - if (counter.samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample group found for bandwidth counter'); - } - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - - // If there are samples before or after hoveredCounter that fall - // horizontally on the same pixel, move hoveredCounter to the sample - // with the highest power value. - const mouseAtTime = (t: number) => - Math.round(((t - rangeStart) / rangeLength) * width + left); - for ( - let currentIndex = hoveredCounter - 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex > 0; - --currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - for ( - let currentIndex = hoveredCounter + 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex < samples.time.length; - ++currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _co2: InstanceType | null = null; - _formatDataTransferValue(bytes: number, l10nId: string) { - if (!this._co2) { - this._co2 = new co2({ model: 'swd' }); - } - // By default when estimating emissions per byte, co2.js takes into account - // emissions for the user device, the data center and the network. - // Because we already have power tracks showing the power use and estimated - // emissions of the device, set the 'device' grid intensity to 0 to avoid - // double counting. - const co2eq = this._co2!.perByteTrace(bytes, false, { - gridIntensity: { device: 0 }, - }); - const carbonValue = formatNumber( - typeof co2eq.co2 === 'number' ? co2eq.co2 : co2eq.co2.total - ); - const value = formatBytes(bytes); - return ( - - {value} - - ); - } - - _renderTooltip(counterIndex: number): React.ReactNode { - const { - accumulatedSamples, - counter, - rangeStart, - rangeEnd, - interval, - previewSelection, - } = this.props; - const { mouseX, mouseY } = this.state; - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No accumulated sample found for bandwidth counter'); - } - - const sampleTime = samples.time[counterIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const bytes = accumulatedCounts[counterIndex] - minCount; - const operations = - samples.number !== undefined ? samples.number[counterIndex] : null; - - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitGraphCount = samples.count[counterIndex] / sampleTimeDeltaInMs; - - let rangeTotal = 0; - if (previewSelection) { - const [beginIndex, endIndex] = getSampleIndexRangeForSelection( - samples, - previewSelection.selectionStart, - previewSelection.selectionEnd - ); - - for ( - let counterSampleIndex = beginIndex; - counterSampleIndex < endIndex; - counterSampleIndex++ - ) { - rangeTotal += samples.count[counterSampleIndex]; - } - } - - let ops; - if (operations !== null) { - ops = formatNumber(operations, 2, 0); - } - - return ( - -
- - {this._formatDataTransferValue( - unitGraphCount * 1000 /* ms -> s */, - 'TrackBandwidthGraph--speed' - )} - {operations !== null ? ( - - {ops} - - ) : null} - - {this._formatDataTransferValue( - bytes, - 'TrackBandwidthGraph--cumulative-bandwidth-at-this-time' - )} - {this._formatDataTransferValue( - countRange, - 'TrackBandwidthGraph--total-bandwidth-in-graph' - )} - {previewSelection - ? this._formatDataTransferValue( - rangeTotal, - 'TrackBandwidthGraph--total-bandwidth-in-range' - ) - : null} - -
-
- ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderBandwidthDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - interval, - } = this.props; - - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for bandwidth counter'); - } - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = (width * (sampleTime - rangeStart)) / rangeLength; - const countRangePerMs = maxCounterSampleCountPerMs; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / sampleTimeDeltaInMs / countRangePerMs; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - accumulatedSamples, - maxCounterSampleCountPerMs, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderBandwidthDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackBandwidthGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - maxCounterSampleCountPerMs: - counterSelectors.getMaxRangeCounterSampleCountPerMs(state), - accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - previewSelection: getPreviewSelection(state), - }; - }, - component: withSize(TrackBandwidthGraphImpl), -}); diff --git a/src/components/timeline/TrackMemory.css b/src/components/timeline/TrackCounter.css similarity index 77% rename from src/components/timeline/TrackMemory.css rename to src/components/timeline/TrackCounter.css index 404b625b0c..c45e73fdf9 100644 --- a/src/components/timeline/TrackMemory.css +++ b/src/components/timeline/TrackCounter.css @@ -2,19 +2,19 @@ * 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/. */ -.timelineTrackMemoryGraph { +.timelineTrackCounterGraph { position: relative; width: 100%; height: var(--graph-height); } -.timelineTrackMemoryCanvas { +.timelineTrackCounterCanvas { position: absolute; width: 100%; height: 100%; } -.timelineTrackMemoryGraphDot { +.timelineTrackCounterGraphDot { position: absolute; width: 6px; height: 6px; @@ -24,18 +24,18 @@ pointer-events: none; } -.timelineTrackMemoryTooltipLine { +.timelineTrackCounterTooltipLine { white-space: nowrap; } -.timelineTrackMemoryTooltipNumber { +.timelineTrackCounterTooltipNumber { display: inline-block; min-width: 60px; color: var(--tooltip-number-foreground-color); font-weight: bold; } -.timelineMarkersMemory { +.timelineTrackCounterMarkers { height: var(--markers-height, 15px); opacity: 1; } diff --git a/src/components/timeline/TrackProcessCPU.tsx b/src/components/timeline/TrackCounter.tsx similarity index 51% rename from src/components/timeline/TrackProcessCPU.tsx rename to src/components/timeline/TrackCounter.tsx index 44b6eb15b6..dfc7c005b2 100644 --- a/src/components/timeline/TrackProcessCPU.tsx +++ b/src/components/timeline/TrackCounter.tsx @@ -8,22 +8,25 @@ import { getCommittedRange, getCounterSelectors, } from 'firefox-profiler/selectors/profile'; +import { TimelineMarkersMemory } from './Markers'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackProcessCPUGraph } from './TrackProcessCPUGraph'; +import { TrackCounterGraph } from './TrackCounterGraph'; import { - TRACK_PROCESS_CPU_HEIGHT, - TRACK_PROCESS_CPU_LINE_WIDTH, + TRACK_COUNTER_GRAPH_HEIGHT, + TRACK_COUNTER_MARKERS_HEIGHT, + TRACK_COUNTER_LINE_WIDTH, } from 'firefox-profiler/app-logic/constants'; import type { CounterIndex, ThreadIndex, Milliseconds, + CounterDisplayConfig, } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; -import './TrackProcessCPU.css'; +import './TrackCounter.css'; type OwnProps = { readonly counterIndex: CounterIndex; @@ -33,6 +36,7 @@ type StateProps = { readonly threadIndex: ThreadIndex; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; + readonly display: CounterDisplayConfig; }; type DispatchProps = { @@ -41,32 +45,56 @@ type DispatchProps = { type Props = ConnectedProps; -type State = {}; +export class TrackCounterImpl extends React.PureComponent { + _onMarkerSelect = (start: Milliseconds, end: Milliseconds) => { + const { rangeStart, rangeEnd, updatePreviewSelection } = this.props; + updatePreviewSelection({ + isModifying: false, + selectionStart: Math.max(rangeStart, start), + selectionEnd: Math.min(rangeEnd, end), + }); + }; -export class TrackProcessCPUImpl extends React.PureComponent { override render() { - const { counterIndex } = this.props; + const { counterIndex, rangeStart, rangeEnd, threadIndex, display } = + this.props; + + const hasMarkers = display.markerSchemaLocation !== null; + const graphHeight = TRACK_COUNTER_GRAPH_HEIGHT; + const totalHeight = hasMarkers + ? graphHeight + TRACK_COUNTER_MARKERS_HEIGHT + : graphHeight; + return (
- + ) : null} +
); } } -export const TrackProcessCPU = explicitConnect< +export const TrackCounter = explicitConnect< OwnProps, StateProps, DispatchProps @@ -80,8 +108,9 @@ export const TrackProcessCPU = explicitConnect< threadIndex: counter.mainThreadIndex, rangeStart: start, rangeEnd: end, + display: counter.display, }; }, mapDispatchToProps: { updatePreviewSelection }, - component: TrackProcessCPUImpl, + component: TrackCounterImpl, }); diff --git a/src/components/timeline/TrackCounterGraph.tsx b/src/components/timeline/TrackCounterGraph.tsx new file mode 100644 index 0000000000..a0f00ee300 --- /dev/null +++ b/src/components/timeline/TrackCounterGraph.tsx @@ -0,0 +1,884 @@ +/* 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/. */ + +import * as React from 'react'; +import { InView } from 'react-intersection-observer'; +import { Localized } from '@fluent/react'; +import { withSize } from 'firefox-profiler/components/shared/WithSize'; +import { + getStrokeColor, + getFillColor, + getDotColor, +} from 'firefox-profiler/profile-logic/graph-color'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { + formatBytes, + formatNumber, + formatPercent, +} from 'firefox-profiler/utils/format-numbers'; +import { bisectionRight } from 'firefox-profiler/utils/bisect'; +import { + getCommittedRange, + getCounterSelectors, + getPreviewSelection, + getProfileInterval, +} from 'firefox-profiler/selectors/profile'; +import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; +import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; +import { TooltipTrackPower } from 'firefox-profiler/components/tooltip/TrackPower'; +import { + TooltipDetails, + TooltipDetail, + TooltipDetailSeparator, +} from 'firefox-profiler/components/tooltip/TooltipDetails'; +import { EmptyThreadIndicator } from './EmptyThreadIndicator'; +import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; +import { co2 } from '@tgwf/co2'; + +import type { + CounterIndex, + Counter, + Thread, + ThreadIndex, + AccumulatedCounterSamples, + Milliseconds, + PreviewSelection, + CssPixels, + StartEndRange, + IndexIntoSamplesTable, +} from 'firefox-profiler/types'; + +import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './TrackCounter.css'; + +/** + * When adding properties to these props, please consider the comment above `TrackCounterCanvas`. + */ +type CanvasProps = { + readonly rangeStart: Milliseconds; + readonly rangeEnd: Milliseconds; + readonly counter: Counter; + readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; + readonly accumulatedSamples: AccumulatedCounterSamples; + readonly maxCounterSampleCountPerMs: number; + readonly interval: Milliseconds; + readonly width: CssPixels; + readonly height: CssPixels; + readonly lineWidth: CssPixels; +}; + +/** + * This component controls the rendering of the canvas. Every render call through + * React triggers a new canvas render. Because of this, it's important to only pass + * in the props that are needed for the canvas draw call. + */ +class TrackCounterCanvas extends React.PureComponent { + _canvas: null | HTMLCanvasElement = null; + _requestedAnimationFrame: boolean = false; + _canvasState: { renderScheduled: boolean; inView: boolean } = { + renderScheduled: false, + inView: false, + }; + + drawCanvas(canvas: HTMLCanvasElement): void { + const { + rangeStart, + rangeEnd, + counter, + height, + width, + lineWidth, + interval, + accumulatedSamples, + maxCounterSampleCountPerMs, + counterSampleRange, + } = this.props; + const { display } = counter; + if (width === 0) { + // Attempt to draw before the canvas was laid out. + return; + } + + const ctx = canvas.getContext('2d')!; + const devicePixelRatio = window.devicePixelRatio; + const deviceWidth = width * devicePixelRatio; + const deviceHeight = height * devicePixelRatio; + const deviceLineWidth = lineWidth * devicePixelRatio; + const deviceLineHalfWidth = deviceLineWidth * 0.5; + const innerDeviceHeight = deviceHeight - deviceLineWidth; + const rangeLength = rangeEnd - rangeStart; + const millisecondWidth = deviceWidth / rangeLength; + const intervalWidth = interval * millisecondWidth; + + // Resize and clear the canvas. + canvas.width = Math.round(deviceWidth); + canvas.height = Math.round(deviceHeight); + ctx.clearRect(0, 0, deviceWidth, deviceHeight); + + const samples = counter.samples; + if (samples.length === 0) { + // There's no reason to draw the samples, there are none. + return; + } + + // Take the sample information, and convert it into chart coordinates. Use a slightly + // smaller space than the deviceHeight, so that the stroke will be fully visible + // both at the top and bottom of the chart. + const [sampleStart, sampleEnd] = counterSampleRange; + + { + // Draw the chart. + // + // ...--` + // 1 ...---```..-- `--. 2 + // |_____________________| + // 4 3 + // + // Start by drawing from 1 to 2. This will be the top of all the peaks of the + // counter graph. + + ctx.lineWidth = deviceLineWidth; + ctx.lineJoin = 'bevel'; + ctx.strokeStyle = getStrokeColor(display.color); + ctx.fillStyle = getFillColor(display.color); + ctx.beginPath(); + + if (display.graphType === 'line-accumulated') { + // Accumulated graph: plot the running total. + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + + // The x and y are used after the loop. + let x = 0; + let y = 0; + let firstX = 0; + for (let i = sampleStart; i < sampleEnd; i++) { + // Create a path for the top of the chart. This is the line that will have + // a stroke applied to it. + x = (samples.time[i] - rangeStart) * millisecondWidth; + // Add on half the stroke's line width so that it won't be cut off the edge + // of the graph. + const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange; + y = + innerDeviceHeight - + innerDeviceHeight * unitGraphCount + + deviceLineHalfWidth; + if (i === sampleStart) { + // This is the first iteration, only move the line, do not draw it. Also + // remember this first X, as the bottom of the graph will need to connect + // back up to it. + firstX = x; + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + // The samples range ends at the time of the last sample, plus the interval. + // Draw this last bit. + ctx.lineTo(x + intervalWidth, y); + + // Don't do the fill yet, just stroke the top line. This will draw a line from + // point 1 to 2 in the diagram above. + ctx.stroke(); + + // After doing the stroke, continue the path to complete the fill to the bottom + // of the canvas. This continues the path to point 3 and then 4. + + // Create a line from 2 to 3. + ctx.lineTo(x + intervalWidth, deviceHeight); + + // Create a line from 3 to 4. + ctx.lineTo(firstX, deviceHeight); + + // The line from 4 to 1 will be implicitly filled in. + ctx.fill(); + } else { + // Rate graph: plot count / timeDelta with min-max decimation. + const countRangePerMs = maxCounterSampleCountPerMs; + + const getX = (i: number) => + Math.round((samples.time[i] - rangeStart) * millisecondWidth); + const getY = (rawY: number) => { + if (!rawY) { + // Make the 0 values invisible so that 'almost 0' is noticeable. + return deviceHeight + deviceLineHalfWidth; + } + const unitGraphCount = rawY / countRangePerMs; + return Math.round( + innerDeviceHeight - + innerDeviceHeight * unitGraphCount + + // Add on half the stroke's line width so that it won't be cut off the edge + // of the graph. + deviceLineHalfWidth + ); + }; + + const getRate = (i: number) => { + const sampleTimeDeltaInMs = + i === 0 ? interval : samples.time[i] - samples.time[i - 1]; + return samples.count[i] / sampleTimeDeltaInMs; + }; + + // The x and y are used after the loop. + const firstX = getX(sampleStart); + let x = firstX; + let y = getY(getRate(sampleStart)); + + // For the first sample, only move the line, do not draw it. Also + // remember this first X, as the bottom of the graph will need to connect + // back up to it. + ctx.moveTo(x, y); + + // Create a path for the top of the chart. This is the line that will have + // a stroke applied to it. + for (let i = sampleStart + 1; i < sampleEnd; i++) { + const rateValues = [getRate(i)]; + x = getX(i); + y = getY(rateValues[0]); + ctx.lineTo(x, y); + + // If we have multiple samples to draw on the same horizontal pixel, + // we process all of them together with a max-min decimation algorithm + // to save time: + // - We draw the first and last samples to ensure the display is + // correct if there are sampling gaps. + // - For the values in between, we only draw the min and max values, + // to draw a vertical line covering all the other sample values. + while (i + 1 < sampleEnd && getX(i + 1) === x) { + rateValues.push(getRate(++i)); + } + + // Looking for the min and max only makes sense if we have more than 2 + // samples to draw. + if (rateValues.length > 2) { + const minY = getY(Math.min(...rateValues)); + if (minY !== y) { + y = minY; + ctx.lineTo(x, y); + } + const maxY = getY(Math.max(...rateValues)); + if (maxY !== y) { + y = maxY; + ctx.lineTo(x, y); + } + } + + const lastY = getY(rateValues[rateValues.length - 1]); + if (lastY !== y) { + y = lastY; + ctx.lineTo(x, y); + } + } + + // The samples range ends at the time of the last sample, plus the interval. + // Draw this last bit. + ctx.lineTo(x + intervalWidth, y); + + // Don't do the fill yet, just stroke the top line. This will draw a line from + // point 1 to 2 in the diagram above. + ctx.stroke(); + + // After doing the stroke, continue the path to complete the fill to the bottom + // of the canvas. This continues the path to point 3 and then 4. + + // Create a line from 2 to 3. + ctx.lineTo(x + intervalWidth, deviceHeight); + + // Create a line from 3 to 4. + ctx.lineTo(firstX, deviceHeight); + + // The line from 4 to 1 will be implicitly filled in. + ctx.fill(); + } + } + } + + _scheduleDraw() { + if (!this._canvasState.inView) { + // Canvas is not in the view. Schedule the render for a later intersection + // observer callback. + this._canvasState.renderScheduled = true; + return; + } + + // Canvas is in the view. Render the canvas and reset the schedule state. + this._canvasState.renderScheduled = false; + + if (!this._requestedAnimationFrame) { + this._requestedAnimationFrame = true; + window.requestAnimationFrame(() => { + this._requestedAnimationFrame = false; + const canvas = this._canvas; + if (canvas) { + this.drawCanvas(canvas); + } + }); + } + } + + _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { + this._canvas = canvas; + }; + + _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { + this._canvasState.inView = inView; + if (!this._canvasState.renderScheduled) { + // Skip if render is not scheduled. + return; + } + + this._scheduleDraw(); + }; + + override componentDidMount() { + this._scheduleDraw(); + } + + override componentDidUpdate() { + this._scheduleDraw(); + } + + override render() { + return ( + + + + ); + } +} + +type OwnProps = { + readonly counterIndex: CounterIndex; + readonly lineWidth: CssPixels; + readonly graphHeight: CssPixels; +}; + +type StateProps = { + readonly threadIndex: ThreadIndex; + readonly rangeStart: Milliseconds; + readonly rangeEnd: Milliseconds; + readonly counter: Counter; + readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; + readonly accumulatedSamples: AccumulatedCounterSamples; + readonly maxCounterSampleCountPerMs: number; + readonly interval: Milliseconds; + readonly filteredThread: Thread; + readonly unfilteredSamplesRange: StartEndRange | null; + readonly previewSelection: PreviewSelection | null; +}; + +type DispatchProps = {}; + +type Props = SizeProps & ConnectedProps; + +type State = { + hoveredCounter: null | number; + mouseX: CssPixels; + mouseY: CssPixels; +}; + +/** + * The generic counter track graph component. It renders information from any counters + * (eg, Memory, Power, etc.) as a graph in the timeline. It branches on + * `display.graphType` for drawing, and on `counter.category`/`counter.name` + * for tooltip rendering of known counter types. + */ +class TrackCounterGraphImpl extends React.PureComponent { + override state = { + hoveredCounter: null, + mouseX: 0, + mouseY: 0, + }; + + _co2: InstanceType | null = null; + + _onMouseLeave = () => { + // This persistTooltips property is part of the web console API. It helps + // in being able to inspect and debug tooltips. + if (window.persistTooltips) { + return; + } + + this.setState({ hoveredCounter: null }); + }; + + _onMouseMove = (event: React.MouseEvent) => { + const { pageX: mouseX, pageY: mouseY } = event; + // Get the offset from here, and apply it to the time lookup. + const { left } = event.currentTarget.getBoundingClientRect(); + const { + width, + rangeStart, + rangeEnd, + counter, + interval, + counterSampleRange, + } = this.props; + const rangeLength = rangeEnd - rangeStart; + const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; + + if (counter.samples.length === 0) { + throw new Error('No sample group found for counter'); + } + const { samples } = counter; + + if ( + timeAtMouse < samples.time[0] || + timeAtMouse > samples.time[samples.length - 1] + interval + ) { + // We are outside the range of the samples, do not display hover information. + this.setState({ hoveredCounter: null }); + } else { + // When the mouse pointer hovers between two points, select the point that's closer. + let hoveredCounter; + const [sampleStart, sampleEnd] = counterSampleRange; + const bisectionCounter = bisectionRight( + samples.time, + timeAtMouse, + sampleStart, + sampleEnd + ); + if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { + const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; + const rightDistance = samples.time[bisectionCounter] - timeAtMouse; + if (leftDistance < rightDistance) { + // Left point is closer + hoveredCounter = bisectionCounter - 1; + } else { + // Right point is closer + hoveredCounter = bisectionCounter; + } + + // For rate-based graphs with decimation, find the sample with the + // highest value at the same pixel position. + if (this.props.counter.display.graphType === 'line-rate') { + const mouseAtTime = (t: number) => + Math.round(((t - rangeStart) / rangeLength) * width + left); + for ( + let currentIndex = hoveredCounter - 1; + mouseAtTime(samples.time[currentIndex]) === mouseX && + currentIndex > 0; + --currentIndex + ) { + if (samples.count[currentIndex] > samples.count[hoveredCounter]) { + hoveredCounter = currentIndex; + } + } + for ( + let currentIndex = hoveredCounter + 1; + mouseAtTime(samples.time[currentIndex]) === mouseX && + currentIndex < samples.time.length; + ++currentIndex + ) { + if (samples.count[currentIndex] > samples.count[hoveredCounter]) { + hoveredCounter = currentIndex; + } + } + } + } else { + hoveredCounter = bisectionCounter; + } + + if (hoveredCounter === samples.length) { + // When hovering the last sample, it's possible the mouse is past the time. + // In this case, hover over the last sample. This happens because of the + // ` + interval` line in the `if` condition above. + hoveredCounter = samples.time.length - 1; + } + + this.setState({ + mouseX, + mouseY, + hoveredCounter, + }); + } + }; + + _formatDataTransferValue(bytes: number, l10nId: string) { + if (!this._co2) { + this._co2 = new co2({ model: 'swd' }); + } + // By default, when estimating emissions per byte, co2.js takes into account + // emissions for the user device, the data center and the network. + // Because we already have power tracks showing the power use and estimated + // emissions of the device, set the 'device' grid intensity to 0 to avoid + // double counting. + const co2eq = this._co2!.perByteTrace(bytes, false, { + gridIntensity: { device: 0 }, + }); + const carbonValue = formatNumber( + typeof co2eq.co2 === 'number' ? co2eq.co2 : co2eq.co2.total + ); + const value = formatBytes(bytes); + return ( + + {value} + + ); + } + + _renderTooltip(counterIndex: number): React.ReactNode { + const { + accumulatedSamples, + counter, + rangeStart, + rangeEnd, + interval, + maxCounterSampleCountPerMs, + previewSelection, + } = this.props; + const { display } = counter; + const { mouseX, mouseY } = this.state; + const { samples } = counter; + + if (samples.length === 0) { + throw new Error('No sample found for counter'); + } + + const sampleTime = samples.time[counterIndex]; + if (sampleTime < rangeStart || sampleTime > rangeEnd) { + // Do not draw the tooltip if it will be rendered outside the timeline. + // This could happen when a sample time is outside the time range. + // While range filtering the counters, we add the sample before start and + // after end, so charts will not be cut off at the edges. + return null; + } + + const { category, name } = counter; + + // Power tooltip — delegate to the dedicated component. + if (category === 'power') { + return ( + + + + ); + } + + // Process CPU tooltip. + if (category === 'CPU' && name === 'processCPU') { + const cpuUsage = samples.count[counterIndex]; + const sampleTimeDeltaInMs = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + const cpuRatio = + cpuUsage / sampleTimeDeltaInMs / maxCounterSampleCountPerMs; + return ( + +
+
+ CPU:{' '} + + {formatPercent(cpuRatio)} + +
+
+
+ ); + } + + // Bandwidth tooltip — bytes with rate, CO2, and accumulated total. + if (category === 'Bandwidth') { + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + const bytes = accumulatedCounts[counterIndex] - minCount; + const operations = + samples.number !== undefined ? samples.number[counterIndex] : null; + + const sampleTimeDeltaInMs = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + const unitGraphCount = samples.count[counterIndex] / sampleTimeDeltaInMs; + + let rangeTotal = 0; + if (previewSelection) { + const [beginIndex, endIndex] = getSampleIndexRangeForSelection( + samples, + previewSelection.selectionStart, + previewSelection.selectionEnd + ); + + for ( + let counterSampleIndex = beginIndex; + counterSampleIndex < endIndex; + counterSampleIndex++ + ) { + rangeTotal += samples.count[counterSampleIndex]; + } + } + + let ops; + if (operations !== null) { + ops = formatNumber(operations, 2, 0); + } + + return ( + +
+ + {this._formatDataTransferValue( + unitGraphCount * 1000 /* ms -> s */, + 'TrackBandwidthGraph--speed' + )} + {operations !== null ? ( + + {ops} + + ) : null} + + {this._formatDataTransferValue( + bytes, + 'TrackBandwidthGraph--cumulative-bandwidth-at-this-time' + )} + {this._formatDataTransferValue( + countRange, + 'TrackBandwidthGraph--total-bandwidth-in-graph' + )} + {previewSelection + ? this._formatDataTransferValue( + rangeTotal, + 'TrackBandwidthGraph--total-bandwidth-in-range' + ) + : null} + +
+
+ ); + } + + // Memory tooltip — accumulated bytes with operations count. + if (category === 'Memory') { + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + const bytes = accumulatedCounts[counterIndex] - minCount; + const operations = + samples.number !== undefined ? samples.number[counterIndex] : null; + return ( + +
+
+ + {formatBytes(bytes)} + + + relative memory at this time + +
+ +
+ + {formatBytes(countRange)} + + + memory range in graph + +
+ {operations !== null ? ( +
+ + {formatNumber(operations, 2, 0)} + + + allocations and deallocations since the previous sample + +
+ ) : null} +
+
+ ); + } + + // Generic tooltip for unknown counter types - format the value based on + // the counter's unit. + const value = samples.count[counterIndex]; + let formattedValue; + if (display.unit === 'bytes') { + formattedValue = formatBytes(value); + } else if (display.unit === 'percent') { + formattedValue = formatPercent(value); + } else if (display.unit) { + // Bypasses i18n but this is hit only for unknown counters. + formattedValue = `${formatNumber(value)} ${display.unit}`; + } else { + formattedValue = formatNumber(value); + } + return ( + +
+
+ + {formattedValue} + + {display.label || name} +
+
+
+ ); + } + + /** + * Create a div that is a dot on top of the graph representing the current + * height of the graph. + */ + _renderDot(counterIndex: number): React.ReactNode { + const { + counter, + rangeStart, + rangeEnd, + graphHeight, + width, + lineWidth, + accumulatedSamples, + maxCounterSampleCountPerMs, + interval, + } = this.props; + + const { samples, display } = counter; + if (samples.length === 0) { + throw new Error('No sample found for counter'); + } + const rangeLength = rangeEnd - rangeStart; + const sampleTime = samples.time[counterIndex]; + + if (sampleTime < rangeStart || sampleTime > rangeEnd) { + // Do not draw the dot if it will be rendered outside the timeline. + // This could happen when a sample time is outside the time range. + // While range filtering the counters, we add the sample before start and + // after end, so charts will not be cut off at the edges. + return null; + } + + const left = (width * (sampleTime - rangeStart)) / rangeLength; + const innerTrackHeight = graphHeight - lineWidth / 2; + let top; + + if (display.graphType === 'line-accumulated') { + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + const unitSampleCount = + (accumulatedCounts[counterIndex] - minCount) / countRange; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + } else { + const sampleTimeDeltaInMs = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + const unitSampleCount = + samples.count[counterIndex] / + sampleTimeDeltaInMs / + maxCounterSampleCountPerMs; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + } + + return ( +
+ ); + } + + override render() { + const { hoveredCounter } = this.state; + const { + filteredThread, + interval, + rangeStart, + rangeEnd, + unfilteredSamplesRange, + counter, + counterSampleRange, + graphHeight, + width, + lineWidth, + accumulatedSamples, + maxCounterSampleCountPerMs, + } = this.props; + + return ( +
+ + {hoveredCounter === null ? null : ( + <> + {this._renderDot(hoveredCounter)} + {this._renderTooltip(hoveredCounter)} + + )} + +
+ ); + } +} + +export const TrackCounterGraph = explicitConnect< + OwnProps, + StateProps, + DispatchProps +>({ + mapStateToProps: (state, ownProps) => { + const { counterIndex } = ownProps; + const counterSelectors = getCounterSelectors(counterIndex); + const counter = counterSelectors.getCounter(state); + const { start, end } = getCommittedRange(state); + const counterSampleRange = + counterSelectors.getCommittedRangeCounterSampleRange(state); + const selectors = getThreadSelectors(counter.mainThreadIndex); + return { + counter, + threadIndex: counter.mainThreadIndex, + accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state), + maxCounterSampleCountPerMs: + counterSelectors.getMaxRangeCounterSampleCountPerMs(state), + rangeStart: start, + rangeEnd: end, + counterSampleRange, + interval: getProfileInterval(state), + filteredThread: selectors.getFilteredThread(state), + unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), + previewSelection: getPreviewSelection(state), + }; + }, + component: withSize(TrackCounterGraphImpl), +}); diff --git a/src/components/timeline/TrackMemory.tsx b/src/components/timeline/TrackMemory.tsx deleted file mode 100644 index 7c0dc3cf00..0000000000 --- a/src/components/timeline/TrackMemory.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - getCommittedRange, - getCounterSelectors, -} from 'firefox-profiler/selectors/profile'; -import { TimelineMarkersMemory } from './Markers'; -import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackMemoryGraph } from './TrackMemoryGraph'; -import { - TRACK_MEMORY_GRAPH_HEIGHT, - TRACK_MEMORY_MARKERS_HEIGHT, - TRACK_MEMORY_LINE_WIDTH, -} from 'firefox-profiler/app-logic/constants'; - -import type { - CounterIndex, - ThreadIndex, - Milliseconds, -} from 'firefox-profiler/types'; - -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackMemory.css'; - -type OwnProps = { - readonly counterIndex: CounterIndex; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; -}; - -type DispatchProps = { - updatePreviewSelection: typeof updatePreviewSelection; -}; - -type Props = ConnectedProps; - -type State = {}; - -export class TrackMemoryImpl extends React.PureComponent { - _onMarkerSelect = (start: Milliseconds, end: Milliseconds) => { - const { rangeStart, rangeEnd, updatePreviewSelection } = this.props; - updatePreviewSelection({ - isModifying: false, - selectionStart: Math.max(rangeStart, start), - selectionEnd: Math.min(rangeEnd, end), - }); - }; - - override render() { - const { counterIndex, rangeStart, rangeEnd, threadIndex } = this.props; - return ( -
- - -
- ); - } -} - -export const TrackMemory = explicitConnect( - { - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - return { - threadIndex: counter.mainThreadIndex, - rangeStart: start, - rangeEnd: end, - }; - }, - mapDispatchToProps: { updatePreviewSelection }, - component: TrackMemoryImpl, - } -); diff --git a/src/components/timeline/TrackMemoryGraph.tsx b/src/components/timeline/TrackMemoryGraph.tsx deleted file mode 100644 index e3d9f4772e..0000000000 --- a/src/components/timeline/TrackMemoryGraph.tsx +++ /dev/null @@ -1,544 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { InView } from 'react-intersection-observer'; -import { Localized } from '@fluent/react'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { - getStrokeColor, - getFillColor, - getDotColor, -} from 'firefox-profiler/profile-logic/graph-color'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - formatBytes, - formatNumber, -} from 'firefox-profiler/utils/format-numbers'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - AccumulatedCounterSamples, - Milliseconds, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackMemory.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackMemoryCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _requestedAnimationFrame: boolean = false; - _canvasState: { renderScheduled: boolean; inView: boolean } = { - renderScheduled: false, - inView: false, - }; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - accumulatedSamples, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - // Take the sample information, and convert it into chart coordinates. Use a slightly - // smaller space than the deviceHeight, so that the stroke will be fully visible - // both at the top and bottom of the chart. - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const [sampleStart, sampleEnd] = counterSampleRange; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // memory graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor(counter.display.color); - ctx.fillStyle = getFillColor(counter.display.color); - ctx.beginPath(); - - // The x and y are used after the loop. - let x = 0; - let y = 0; - let firstX = 0; - for (let i = sampleStart; i < sampleEnd; i++) { - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - x = (samples.time[i] - rangeStart) * millisecondWidth; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange; - y = - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth; - if (i === 0) { - // This is the first iteration, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - firstX = x; - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - } - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _scheduleDraw() { - if (!this._canvasState.inView) { - // Canvas is not in the view. Schedule the render for a later intersection - // observer callback. - this._canvasState.renderScheduled = true; - return; - } - - // Canvas is in the view. Render the canvas and reset the schedule state. - this._canvasState.renderScheduled = false; - - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - const canvas = this._canvas; - if (canvas) { - this.drawCanvas(canvas); - } - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { - this._canvasState.inView = inView; - if (!this._canvasState.renderScheduled) { - // Skip if render is not scheduled. - return; - } - - this._scheduleDraw(); - }; - - override componentDidMount() { - this._scheduleDraw(); - } - - override componentDidUpdate() { - this._scheduleDraw(); - } - - override render() { - return ( - - - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The memory track graph takes memory information from counters, and renders it as a - * graph in the timeline. - */ -class TrackMemoryGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - // This persistTooltips property is part of the web console API. It helps - // in being able to inspect and debug tooltips. - if (window.persistTooltips) { - return; - } - - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - if (counter.samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample group found for memory counter'); - } - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _renderTooltip(counterIndex: number): React.ReactNode { - const { accumulatedSamples, counter, rangeStart, rangeEnd } = this.props; - const { mouseX, mouseY } = this.state; - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No accumulated sample found for memory counter'); - } - - const sampleTime = samples.time[counterIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const bytes = accumulatedCounts[counterIndex] - minCount; - const operations = - samples.number !== undefined ? samples.number[counterIndex] : null; - return ( - -
-
- - {formatBytes(bytes)} - - - relative memory at this time - -
- -
- - {formatBytes(countRange)} - - - memory range in graph - -
- {operations !== null ? ( -
- - {formatNumber(operations, 2, 0)} - - - allocations and deallocations since the previous sample - -
- ) : null} -
-
- ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderMemoryDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - accumulatedSamples, - } = this.props; - - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for memory counter'); - } - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = (width * (sampleTime - rangeStart)) / rangeLength; - - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const unitSampleCount = - (accumulatedCounts[counterIndex] - minCount) / countRange; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - accumulatedSamples, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderMemoryDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackMemoryGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - }; - }, - component: withSize(TrackMemoryGraphImpl), -}); diff --git a/src/components/timeline/TrackPower.css b/src/components/timeline/TrackPower.css deleted file mode 100644 index 09f616a1c2..0000000000 --- a/src/components/timeline/TrackPower.css +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.timelineTrackPowerGraph { - position: relative; - width: 100%; - height: var(--graph-height); -} - -.timelineTrackPowerCanvas { - position: absolute; - width: 100%; - height: 100%; -} - -.timelineTrackPowerGraphDot { - position: absolute; - width: 6px; - height: 6px; - border-radius: 3px; - margin-top: -3px; - margin-left: -3px; - pointer-events: none; -} diff --git a/src/components/timeline/TrackPower.tsx b/src/components/timeline/TrackPower.tsx deleted file mode 100644 index 3272f09c19..0000000000 --- a/src/components/timeline/TrackPower.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - getCommittedRange, - getCounterSelectors, -} from 'firefox-profiler/selectors/profile'; -import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackPowerGraph } from './TrackPowerGraph'; -import { - TRACK_POWER_HEIGHT, - TRACK_POWER_LINE_WIDTH, -} from 'firefox-profiler/app-logic/constants'; - -import type { - CounterIndex, - ThreadIndex, - Milliseconds, -} from 'firefox-profiler/types'; - -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackPower.css'; - -type OwnProps = { - readonly counterIndex: CounterIndex; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; -}; - -type DispatchProps = { - updatePreviewSelection: typeof updatePreviewSelection; -}; - -type Props = ConnectedProps; - -type State = {}; - -export class TrackPowerImpl extends React.PureComponent { - override render() { - const { counterIndex } = this.props; - return ( -
- -
- ); - } -} - -export const TrackPower = explicitConnect({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - return { - threadIndex: counter.mainThreadIndex, - rangeStart: start, - rangeEnd: end, - }; - }, - mapDispatchToProps: { updatePreviewSelection }, - component: TrackPowerImpl, -}); diff --git a/src/components/timeline/TrackPowerGraph.tsx b/src/components/timeline/TrackPowerGraph.tsx deleted file mode 100644 index fe654222dd..0000000000 --- a/src/components/timeline/TrackPowerGraph.tsx +++ /dev/null @@ -1,569 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { InView } from 'react-intersection-observer'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { - getStrokeColor, - getFillColor, - getDotColor, -} from 'firefox-profiler/profile-logic/graph-color'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { TooltipTrackPower } from 'firefox-profiler/components/tooltip/TrackPower'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - Milliseconds, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; -import { timeCode } from 'firefox-profiler/utils/time-code'; - -import './TrackPower.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackPowerCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _canvasState: { renderScheduled: boolean; inView: boolean } = { - renderScheduled: false, - inView: false, - }; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - maxCounterSampleCountPerMs, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - const [sampleStart, sampleEnd] = counterSampleRange; - const countRangePerMs = maxCounterSampleCountPerMs; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // power graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor(counter.display.color); - ctx.fillStyle = getFillColor(counter.display.color); - ctx.beginPath(); - - const getX = (i: number) => - Math.round((samples.time[i] - rangeStart) * millisecondWidth); - const getPower = (i: number) => { - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - return samples.count[i] / sampleTimeDeltaInMs; - }; - const getY = (rawY: number) => { - if (!rawY) { - // Make the 0 values invisible so that 'almost 0' is noticeable. - return deviceHeight + deviceLineHalfWidth; - } - - const unitGraphCount = rawY / countRangePerMs; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - return Math.round( - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth - ); - }; - - // The x and y are used after the loop. - const firstX = getX(sampleStart); - let x = firstX; - let y = getY(getPower(sampleStart)); - - // For the first sample, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - ctx.moveTo(x, y); - - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - for (let i = sampleStart + 1; i < sampleEnd; i++) { - const powerValues = [getPower(i)]; - x = getX(i); - y = getY(powerValues[0]); - ctx.lineTo(x, y); - - // If we have multiple samples to draw on the same horizontal pixel, - // we process all of them together with a max-min decimation algorithm - // to save time: - // - We draw the first and last samples to ensure the display is - // correct if there are sampling gaps. - // - For the values in between, we only draw the min and max values, - // to draw a vertical line covering all the other sample values. - while (i + 1 < sampleEnd && getX(i + 1) === x) { - powerValues.push(getPower(++i)); - } - - // Looking for the min and max only makes sense if we have more than 2 - // samples to draw. - if (powerValues.length > 2) { - const minY = getY(Math.min(...powerValues)); - if (minY !== y) { - y = minY; - ctx.lineTo(x, y); - } - const maxY = getY(Math.max(...powerValues)); - if (maxY !== y) { - y = maxY; - ctx.lineTo(x, y); - } - } - - const lastY = getY(powerValues[powerValues.length - 1]); - if (lastY !== y) { - y = lastY; - ctx.lineTo(x, y); - } - } - - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _renderCanvas() { - if (!this._canvasState.inView) { - // Canvas is not in the view. Schedule the render for a later intersection - // observer callback. - this._canvasState.renderScheduled = true; - return; - } - - // Canvas is in the view. Render the canvas and reset the schedule state. - this._canvasState.renderScheduled = false; - - const canvas = this._canvas; - if (canvas) { - timeCode('TrackPowerCanvas render', () => { - this.drawCanvas(canvas); - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { - this._canvasState.inView = inView; - if (!this._canvasState.renderScheduled) { - // Skip if render is not scheduled. - return; - } - - this._renderCanvas(); - }; - - override render() { - this._renderCanvas(); - - return ( - - - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The power track graph takes power use information from counters, and renders it as a - * graph in the timeline. - */ -class TrackPowerGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - - // If there are samples before or after hoveredCounter that fall - // horizontally on the same pixel, move hoveredCounter to the sample - // with the highest power value. - const mouseAtTime = (t: number) => - Math.round(((t - rangeStart) / rangeLength) * width + left); - for ( - let currentIndex = hoveredCounter - 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex > 0; - --currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - for ( - let currentIndex = hoveredCounter + 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex < samples.time.length; - ++currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _renderTooltip(counterSampleIndex: number): React.ReactNode { - const { counter, rangeStart, rangeEnd } = this.props; - const { mouseX, mouseY } = this.state; - - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for power counter'); - } - - const sampleTime = samples.time[counterSampleIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - return ( - - - - ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - interval, - } = this.props; - - const { samples } = counter; - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = (width * (sampleTime - rangeStart)) / rangeLength; - - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // power counter. Print an error and bail out early. - throw new Error('No sample found for power counter'); - } - const countRangePerMs = maxCounterSampleCountPerMs; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / sampleTimeDeltaInMs / countRangePerMs; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackPowerGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - maxCounterSampleCountPerMs: - counterSelectors.getMaxRangeCounterSampleCountPerMs(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - }; - }, - component: withSize(TrackPowerGraphImpl), -}); diff --git a/src/components/timeline/TrackProcessCPU.css b/src/components/timeline/TrackProcessCPU.css deleted file mode 100644 index 76f8c45d52..0000000000 --- a/src/components/timeline/TrackProcessCPU.css +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.timelineTrackProcessCPUGraph { - position: relative; - width: 100%; - height: var(--graph-height); -} - -.timelineTrackProcessCPUCanvas { - position: absolute; - width: 100%; - height: 100%; -} - -.timelineTrackProcessCPUGraphDot { - --internal-background-color: var(--grey-50); - - position: absolute; - width: 6px; - height: 6px; - border-radius: 3px; - margin-top: -3px; - margin-left: -3px; - background-color: var(--internal-background-color); - pointer-events: none; -} - -.timelineTrackProcessCPUTooltipLine { - white-space: nowrap; -} - -.timelineTrackProcessCPUTooltipNumber { - display: inline-block; - min-width: 60px; - color: var(--tooltip-number-foreground-color); - font-weight: bold; -} diff --git a/src/components/timeline/TrackProcessCPUGraph.tsx b/src/components/timeline/TrackProcessCPUGraph.tsx deleted file mode 100644 index 0f6c7d7fe3..0000000000 --- a/src/components/timeline/TrackProcessCPUGraph.tsx +++ /dev/null @@ -1,477 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { formatPercent } from 'firefox-profiler/utils/format-numbers'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { GREY_50 } from 'photon-colors'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - Milliseconds, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackProcessCPU.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackProcessCPUCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _requestedAnimationFrame: boolean = false; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - maxCounterSampleCountPerMs, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - const [sampleStart, sampleEnd] = counterSampleRange; - const countRangePerMs = maxCounterSampleCountPerMs; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // process CPU graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = GREY_50; - ctx.fillStyle = '#73737388'; // Grey 50 with transparency. - ctx.beginPath(); - - // The x and y are used after the loop. - let x = 0; - let y = 0; - let firstX = 0; - for (let i = sampleStart; i < sampleEnd; i++) { - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - x = (samples.time[i] - rangeStart) * millisecondWidth; - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - const unitGraphCount = - samples.count[i] / sampleTimeDeltaInMs / countRangePerMs; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - y = - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth; - if (i === 0) { - // This is the first iteration, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - firstX = x; - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - } - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _scheduleDraw() { - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - const canvas = this._canvas; - if (canvas) { - this.drawCanvas(canvas); - } - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - override render() { - this._scheduleDraw(); - - return ( - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The process CPU track graph takes CPU information from counters, and renders it as a - * graph in the timeline. - */ -class TrackProcessCPUGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _renderTooltip(counterIndex: number): React.ReactNode { - const { - counter, - maxCounterSampleCountPerMs, - interval, - rangeStart, - rangeEnd, - } = this.props; - const { mouseX, mouseY } = this.state; - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for process CPU counter'); - } - const sampleTime = samples.time[counterIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const maxCPUPerMs = maxCounterSampleCountPerMs; - const cpuUsage = samples.count[counterIndex]; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const cpuRatio = cpuUsage / sampleTimeDeltaInMs / maxCPUPerMs; - return ( - -
-
- CPU:{' '} - - {formatPercent(cpuRatio)} - -
-
-
- ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - interval, - } = this.props; - const { samples } = counter; - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = - (width * (samples.time[counterIndex] - rangeStart)) / rangeLength; - - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // process CPU counter. Print an error and bail out early. - throw new Error('No sample found for process CPU counter'); - } - const countRangePerMs = maxCounterSampleCountPerMs; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / sampleTimeDeltaInMs / countRangePerMs; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackProcessCPUGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - maxCounterSampleCountPerMs: - counterSelectors.getMaxCounterSampleCountPerMs(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - }; - }, - component: withSize(TrackProcessCPUGraphImpl), -}); diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index d95355d18d..c760201465 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -1013,7 +1013,7 @@ function _deriveCounterDisplay( unit: 'percent', color: 'grey', markerSchemaLocation: null, - sortWeight: 40, + sortWeight: 70, label: 'Process CPU', }; } @@ -1023,7 +1023,7 @@ function _deriveCounterDisplay( unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 50, + sortWeight: 35, label: name, }; } diff --git a/src/profile-logic/tracks.ts b/src/profile-logic/tracks.ts index c831ad796e..4e61ddf608 100644 --- a/src/profile-logic/tracks.ts +++ b/src/profile-logic/tracks.ts @@ -56,30 +56,49 @@ export type HiddenTracks = { const LOCAL_TRACK_INDEX_ORDER = { thread: 0, network: 1, - memory: 2, + // Needed only for ts types; counter tracks use _getCounterTrackIndexOrder(). + counter: -1, ipc: 3, 'event-delay': 4, - 'process-cpu': 5, - power: 6, marker: 7, - bandwidth: 8, }; const LOCAL_TRACK_DISPLAY_ORDER = { network: 0, - bandwidth: 1, - memory: 2, - power: 3, + // Needed only for ts types; counter tracks use display.sortWeight. + counter: -1, // IPC tracks that belong to the global track will appear right after network // and counter tracks. But we want to show the IPC tracks that belong to the // local threads right after their track. This special handling happens inside // the sort function. - ipc: 4, - thread: 5, - 'event-delay': 6, - 'process-cpu': 7, - marker: 8, + ipc: 40, + thread: 50, + 'event-delay': 60, + marker: 80, }; +/** + * Map a counter's category and name to the old LOCAL_TRACK_INDEX_ORDER value + * that was used before all counter types were unified. This is needed for + * backward compatibility of URL-encoded track indexes. + */ +function _getCounterTrackIndexOrder(counter: RawCounter): number { + const { category, name } = counter; + if (category === 'Memory') { + return 2; + } + if (category === 'CPU' && name === 'processCPU') { + return 5; + } + if (category === 'power') { + return 6; + } + if (category === 'Bandwidth') { + return 8; + } + // Unknown counter types go after all known types. + return 9; +} + const GLOBAL_TRACK_INDEX_ORDER = { process: 0, screenshots: 1, @@ -126,17 +145,22 @@ function _getDefaultLocalTrackOrder( if ( profile && profile.counters && - tracks[a].type === 'power' && - tracks[b].type === 'power' + tracks[a].type === 'counter' && + tracks[b].type === 'counter' ) { - const idxA = tracks[a].counterIndex; - const idxB = tracks[b].counterIndex; + const counterA = profile.counters[tracks[a].counterIndex]; + const counterB = profile.counters[tracks[b].counterIndex]; + // Sort counter tracks by their display.sortWeight first. + const sortWeightDiff = + counterA.display.sortWeight - counterB.display.sortWeight; + if (sortWeightDiff !== 0) { + return sortWeightDiff; + } + // Within the same sortWeight, sort by name. if (profile.meta.keepProfileThreadOrder) { - return idxA - idxB; + return tracks[a].counterIndex - tracks[b].counterIndex; } - const nameA = profile.counters[idxA].name; - const nameB = profile.counters[idxB].name; - return naturalSort.compare(nameA, nameB); + return naturalSort.compare(counterA.name, counterB.name); } // If the tracks are both threads, sort them by thread name, and then by @@ -158,10 +182,15 @@ function _getDefaultLocalTrackOrder( ); } - return ( - LOCAL_TRACK_DISPLAY_ORDER[tracks[a].type] - - LOCAL_TRACK_DISPLAY_ORDER[tracks[b].type] - ); + const displayA = + tracks[a].type === 'counter' && profile && profile.counters + ? profile.counters[tracks[a].counterIndex].display.sortWeight + : LOCAL_TRACK_DISPLAY_ORDER[tracks[a].type]; + const displayB = + tracks[b].type === 'counter' && profile && profile.counters + ? profile.counters[tracks[b].counterIndex].display.sortWeight + : LOCAL_TRACK_DISPLAY_ORDER[tracks[b].type]; + return displayA - displayB; }); return trackOrder; @@ -400,30 +429,29 @@ export function computeLocalTracksByPid( const { counters } = profile; if (counters) { for (let counterIndex = 0; counterIndex < counters.length; counterIndex++) { - const { pid, category, samples } = counters[counterIndex]; + const { pid, category, name, samples } = counters[counterIndex]; if (!availablePids.has(pid)) { // If the global track is filtered out ignore it here too. continue; } - if (['Memory', 'power', 'Bandwidth'].includes(category)) { - if (category === 'power' && samples.length <= 2) { - // If we have only 2 samples, they are likely both 0 and we don't have a real counter. - continue; - } - let tracks = localTracksByPid.get(pid); - if (tracks === undefined) { - tracks = []; - localTracksByPid.set(pid, tracks); - } - if (category === 'Memory') { - tracks.push({ type: 'memory', counterIndex }); - } else if (category === 'Bandwidth') { - tracks.push({ type: 'bandwidth', counterIndex }); - } else { - tracks.push({ type: 'power', counterIndex }); - } + // Skip processCPU counters — they are added separately by + // addProcessCPUTracksForProcess when the experimental flag is enabled. + if (category === 'CPU' && name === 'processCPU') { + continue; + } + + if (category === 'power' && samples.length <= 2) { + // If we have only 2 samples, they are likely both 0 and we don't have a real counter. + continue; + } + + let tracks = localTracksByPid.get(pid); + if (tracks === undefined) { + tracks = []; + localTracksByPid.set(pid, tracks); } + tracks.push({ type: 'counter', counterIndex }); } } @@ -431,10 +459,17 @@ export function computeLocalTracksByPid( // added at the end so that the local track indexes are stable and backwards compatible. for (const localTracks of localTracksByPid.values()) { // In place sort! - localTracks.sort( - (a: LocalTrack, b: LocalTrack) => - LOCAL_TRACK_INDEX_ORDER[a.type] - LOCAL_TRACK_INDEX_ORDER[b.type] - ); + localTracks.sort((a: LocalTrack, b: LocalTrack) => { + const orderA = + a.type === 'counter' && counters + ? _getCounterTrackIndexOrder(counters[a.counterIndex]) + : LOCAL_TRACK_INDEX_ORDER[a.type]; + const orderB = + b.type === 'counter' && counters + ? _getCounterTrackIndexOrder(counters[b.counterIndex]) + : LOCAL_TRACK_INDEX_ORDER[b.type]; + return orderA - orderB; + }); } return localTracksByPid; @@ -496,7 +531,7 @@ export function addProcessCPUTracksForProcess( let localTracks = newLocalTracksByPid.get(pid) ?? []; // Do not mutate the current state. - localTracks = [...localTracks, { type: 'process-cpu', counterIndex }]; + localTracks = [...localTracks, { type: 'counter', counterIndex }]; newLocalTracksByPid.set(pid, localTracks); } @@ -1116,10 +1151,10 @@ export function getLocalTrackName( return getFriendlyThreadName(threads, threads[localTrack.threadIndex]); case 'network': return 'Network'; - case 'memory': - return 'Memory'; - case 'bandwidth': - return 'Bandwidth'; + case 'counter': { + const counter = counters[localTrack.counterIndex]; + return counter.display.label || counter.name; + } case 'ipc': return `IPC — ${getFriendlyThreadName( threads, @@ -1130,10 +1165,6 @@ export function getLocalTrackName( getFriendlyThreadName(threads, threads[localTrack.threadIndex]) + ' Event Delay' ); - case 'process-cpu': - return 'Process CPU'; - case 'power': - return counters[localTrack.counterIndex].name; case 'marker': return shared.stringArray[localTrack.markerName]; default: @@ -1577,14 +1608,20 @@ export function getSearchFilteredLocalTracksByPid( } break; } + case 'counter': { + // Match against the counter's display label (e.g., "Memory", + // "Bandwidth") rather than the generic type string 'counter'. + const trackName = localTrackNames[trackIndex]; + if (searchRegExp.test(trackName)) { + searchFilteredLocalTracks.add(trackIndex); + continue; + } + break; + } case 'network': - case 'memory': - case 'bandwidth': case 'marker': case 'ipc': - case 'event-delay': - case 'power': - case 'process-cpu': { + case 'event-delay': { const { type } = localTrack; if (searchRegExp.test(type)) { searchFilteredLocalTracks.add(trackIndex); @@ -1761,8 +1798,6 @@ export function getTrackReferenceFromThreadIndex( * If the track is not a thread, some of them can be visible by default and some * of them can be hidden to reduce the noise. This mostly depends on either the * usefulness or the activity of that track. - * - * TODO: Check the memory track activity here to decide if it should be visible. */ function _isLocalTrackVisible( localTrack: LocalTrack, @@ -1774,15 +1809,10 @@ function _isLocalTrackVisible( return visibleThreadIndexes.has(localTrack.threadIndex); case 'marker': case 'network': - case 'memory': - case 'bandwidth': - // 'event-delay' and 'process-cpu' tracks are experimental and they should - // be visible by default whenever they are included in a profile. (fallthrough) + case 'counter': + // 'event-delay' track is experimental, and it should be visible by default + // whenever it is included in a profile. (fallthrough) case 'event-delay': - case 'process-cpu': - // Power tracks are there only if the power feature is enabled. So they should - // be visible by default whenever they're included in a profile. (fallthrough) - case 'power': // Keep non-thread local tracks visible. return true; case 'ipc': diff --git a/src/selectors/app.tsx b/src/selectors/app.tsx index ec78760d84..e978f606c2 100644 --- a/src/selectors/app.tsx +++ b/src/selectors/app.tsx @@ -9,21 +9,20 @@ import { getHiddenGlobalTracks, getHiddenLocalTracksByPid, } from './url-state'; -import { getGlobalTracks, getLocalTracksByPid } from './profile'; +import { getGlobalTracks, getLocalTracksByPid, getCounters } from './profile'; import { getZipFileState } from './zipped-profiles'; import { assertExhaustiveCheck, ensureExists } from '../utils/types'; import { FULL_TRACK_SCREENSHOT_HEIGHT, TRACK_NETWORK_HEIGHT, - TRACK_MEMORY_HEIGHT, - TRACK_BANDWIDTH_HEIGHT, TRACK_IPC_HEIGHT, TRACK_PROCESS_BLANK_HEIGHT, TIMELINE_RULER_HEIGHT, TRACK_VISUAL_PROGRESS_HEIGHT, TRACK_EVENT_DELAY_HEIGHT, - TRACK_PROCESS_CPU_HEIGHT, TRACK_MARKER_HEIGHT, + TRACK_COUNTER_GRAPH_HEIGHT, + TRACK_COUNTER_MARKERS_HEIGHT, } from '../app-logic/constants'; import type { @@ -104,12 +103,14 @@ export const getTimelineHeight: Selector = createSelector( getHiddenGlobalTracks, getHiddenLocalTracksByPid, getTrackThreadHeights, + getCounters, ( globalTracks, localTracksByPid, hiddenGlobalTracks, hiddenLocalTracksByPid, - trackThreadHeights + trackThreadHeights, + counters ) => { let height = TIMELINE_RULER_HEIGHT; const border = 1; @@ -184,22 +185,24 @@ export const getTimelineHeight: Selector = createSelector( case 'network': height += TRACK_NETWORK_HEIGHT + border; break; - case 'memory': - height += TRACK_MEMORY_HEIGHT + border; - break; - case 'bandwidth': - height += TRACK_BANDWIDTH_HEIGHT + border; + case 'counter': { + // Counter track height depends on whether it has markers. + const counter = counters?.[localTrack.counterIndex]; + const hasMarkers = + counter?.display.markerSchemaLocation !== null && + counter?.display.markerSchemaLocation !== undefined; + height += hasMarkers + ? TRACK_COUNTER_GRAPH_HEIGHT + TRACK_COUNTER_MARKERS_HEIGHT + : TRACK_COUNTER_GRAPH_HEIGHT; + height += border; break; + } case 'event-delay': height += TRACK_EVENT_DELAY_HEIGHT + border; break; case 'ipc': height += TRACK_IPC_HEIGHT + border; break; - case 'process-cpu': - case 'power': - height += TRACK_PROCESS_CPU_HEIGHT + border; - break; case 'marker': height += TRACK_MARKER_HEIGHT + border; break; diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 7ac0ef4f2c..9501c18fda 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -549,10 +549,19 @@ export const getLocalTrackFromReference: DangerousSelectorWithArguments< */ export const getProcessesWithMemoryTrack: Selector> = createSelector( getLocalTracksByPid, - (localTracksByPid) => { + getCounters, + (localTracksByPid, counters) => { const processesWithMemoryTrack = new Set(); for (const [pid, localTracks] of localTracksByPid.entries()) { - if (localTracks.some((track) => track.type === 'memory')) { + if ( + localTracks.some( + (track) => + track.type === 'counter' && + counters !== null && + counters[track.counterIndex].display.markerSchemaLocation === + 'timeline-memory' + ) + ) { processesWithMemoryTrack.add(pid); } } diff --git a/src/test/components/TrackBandwidth.test.tsx b/src/test/components/TrackBandwidth.test.tsx index 609fb05856..f019d06bb5 100644 --- a/src/test/components/TrackBandwidth.test.tsx +++ b/src/test/components/TrackBandwidth.test.tsx @@ -13,7 +13,7 @@ import { } from 'firefox-profiler/test/fixtures/testing-library'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackBandwidth } from '../../components/timeline/TrackBandwidth'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -79,11 +79,15 @@ describe('TrackBandwidth', function () { length: SAMPLE_COUNT, }, 'SystemBandwidth', - 'bandwidth' + 'Bandwidth' ); counter.display = { ...counter.display, + graphType: 'line-rate', + unit: 'bytes', color: 'blue', + sortWeight: 10, + label: 'Bandwidth', }; profile.counters = [counter]; const store = storeWithProfile(profile); @@ -92,7 +96,7 @@ describe('TrackBandwidth', function () { const renderResult = render( - + ); const { container } = renderResult; @@ -101,13 +105,13 @@ describe('TrackBandwidth', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackBandwidthCanvas'), - `Couldn't find the bandwidth canvas, with selector .timelineTrackBandwidthCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the bandwidth canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackBandwidthTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getBandwidthDot = () => - container.querySelector('.timelineTrackBandwidthGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, diff --git a/src/test/components/TrackMemory.test.tsx b/src/test/components/TrackMemory.test.tsx index add3f14fea..757293cd48 100644 --- a/src/test/components/TrackMemory.test.tsx +++ b/src/test/components/TrackMemory.test.tsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import { render } from 'firefox-profiler/test/fixtures/testing-library'; -import { TrackMemory } from '../../components/timeline/TrackMemory'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -64,9 +64,15 @@ describe('TrackMemory', function () { const threadIndex = 0; const thread = profile.threads[threadIndex]; const counter = getCounterForThread(thread, threadIndex, counterConfig); + counter.category = 'Memory'; counter.display = { ...counter.display, + graphType: 'line-accumulated', + unit: 'bytes', color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', }; profile.counters = [counter]; const store = storeWithProfile(profile); @@ -75,7 +81,7 @@ describe('TrackMemory', function () { const renderResult = render( - + ); const { container } = renderResult; @@ -84,13 +90,13 @@ describe('TrackMemory', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackMemoryCanvas'), - `Couldn't find the memory canvas, with selector .timelineTrackMemoryCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the memory canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackMemoryTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getMemoryDot = () => - container.querySelector('.timelineTrackMemoryGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, @@ -197,7 +203,7 @@ describe('TrackMemory with intersection observer', function () { const renderResult = render( - + ); diff --git a/src/test/components/TrackPower.test.tsx b/src/test/components/TrackPower.test.tsx index 402180cec9..c952a33f35 100644 --- a/src/test/components/TrackPower.test.tsx +++ b/src/test/components/TrackPower.test.tsx @@ -9,7 +9,7 @@ import { fireEvent } from '@testing-library/react'; import { render, screen } from 'firefox-profiler/test/fixtures/testing-library'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackPower } from '../../components/timeline/TrackPower'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -62,30 +62,36 @@ describe('TrackPower', function () { for (let i = 7; i < sampleTimes.length - 1; ++i) { sampleTimes[i] = 7 + i / 100; } - profile.counters = [ - getCounterForThreadWithSamples( - thread, - threadIndex, - { - time: sampleTimes.slice(), - // Power usage numbers. They are pWh so they are pretty big. - count: [ - 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, - 20000, 1, 12000, 100000, - ], - length: SAMPLE_COUNT, - }, - 'SystemPower', - 'power' - ), - ]; + const counter = getCounterForThreadWithSamples( + thread, + threadIndex, + { + time: sampleTimes.slice(), + // Power usage numbers. They are pWh so they are pretty big. + count: [ + 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, 20000, + 1, 12000, 100000, + ], + length: SAMPLE_COUNT, + }, + 'SystemPower', + 'power' + ); + counter.display = { + ...counter.display, + graphType: 'line-rate', + unit: 'pWh', + sortWeight: 30, + label: 'SystemPower', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); const renderResult = render( - + ); const { container } = renderResult; @@ -94,13 +100,13 @@ describe('TrackPower', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackPowerCanvas'), - `Couldn't find the power canvas, with selector .timelineTrackPowerCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the power canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => document.querySelector('.timelineTrackPowerTooltip'); const getPowerDot = () => - container.querySelector('.timelineTrackPowerGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, diff --git a/src/test/components/TrackProcessCPU.test.tsx b/src/test/components/TrackProcessCPU.test.tsx index 0fc742f5e4..9772159547 100644 --- a/src/test/components/TrackProcessCPU.test.tsx +++ b/src/test/components/TrackProcessCPU.test.tsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import { render, screen } from 'firefox-profiler/test/fixtures/testing-library'; -import { TrackProcessCPU } from '../../components/timeline/TrackProcessCPU'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -55,27 +55,33 @@ describe('TrackProcessCPU', function () { const sampleTimes = ensureExists(thread.samples.time); // Changing one of the sample times, so we can test different intervals. sampleTimes[1] = 1.5; // It was 1 before. - profile.counters = [ - getCounterForThreadWithSamples( - thread, - threadIndex, - { - time: sampleTimes.slice(), - // CPU usage numbers for the per-process CPU. - count: [100, 400, 500, 1000, 200, 500, 300, 100], - length: SAMPLE_COUNT, - }, - 'processCPU', - 'CPU' - ), - ]; + const counter = getCounterForThreadWithSamples( + thread, + threadIndex, + { + time: sampleTimes.slice(), + // CPU usage numbers for the per-process CPU. + count: [100, 400, 500, 1000, 200, 500, 300, 100], + length: SAMPLE_COUNT, + }, + 'processCPU', + 'CPU' + ); + counter.display = { + ...counter.display, + graphType: 'line-rate', + unit: 'percent', + sortWeight: 70, + label: 'Process CPU', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); const renderResult = render( - + ); const { container } = renderResult; @@ -84,13 +90,13 @@ describe('TrackProcessCPU', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackProcessCPUCanvas'), - `Couldn't find the process CPU canvas, with selector .timelineTrackProcessCPUCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the process CPU canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackProcessCPUTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getProcessCPUDot = () => - container.querySelector('.timelineTrackProcessCPUGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, diff --git a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap index f1c568e5db..1e2bddd3a9 100644 --- a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap @@ -27,7 +27,7 @@ exports[`timeline/LocalTrack with a memory track matches the snapshot of the mem class="timelineTrackTrack" >
diff --git a/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap b/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap index 26028f6e40..a5e876b61a 100644 --- a/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap @@ -2,7 +2,7 @@ exports[`TrackBandwidth draws a dot that matches the snapshot 1`] = `
`; @@ -43,22 +43,22 @@ Array [ Array [ "lineTo", 15, - 23.877333333333333, + 24, ], Array [ "lineTo", 20, - 23.54, + 24, ], Array [ "lineTo", 30, - 23.54, + 24, ], Array [ "lineTo", 40, - 14.799999999999999, + 15, ], Array [ "lineTo", @@ -68,37 +68,37 @@ Array [ Array [ "lineTo", 60, - 23.862, + 24, ], Array [ "lineTo", 71, - 19.700934579439256, + 20, ], Array [ "lineTo", 71, - 23.99954, + 24, ], Array [ "lineTo", 71, - 14.799999999999804, + 15, ], Array [ "lineTo", 71, - 18.479999999999883, + 18, ], Array [ "lineTo", 110, - 23.882051282051282, + 24, ], Array [ "lineTo", 120, - 23.882051282051282, + 24, ], Array [ "stroke", @@ -121,15 +121,15 @@ Array [ exports[`TrackBandwidth matches the component snapshot 1`] = `
diff --git a/src/test/components/__snapshots__/TrackMemory.test.tsx.snap b/src/test/components/__snapshots__/TrackMemory.test.tsx.snap index 60376ab22d..eadc991dd5 100644 --- a/src/test/components/__snapshots__/TrackMemory.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackMemory.test.tsx.snap @@ -2,40 +2,40 @@ exports[`TrackMemory draws a dot that matches the snapshot 1`] = `
`; exports[`TrackMemory has a tooltip that matches the snapshot 1`] = `
0B relative memory at this time
2B memory range in graph
36 @@ -46,23 +46,23 @@ exports[`TrackMemory has a tooltip that matches the snapshot 1`] = ` exports[`TrackMemory has a tooltip that matches the snapshot for counts equalling zero 1`] = `
0B relative memory at this time
2B @@ -182,7 +182,7 @@ Array [ exports[`TrackMemory matches the component snapshot 1`] = `
diff --git a/src/test/components/__snapshots__/TrackPower.test.tsx.snap b/src/test/components/__snapshots__/TrackPower.test.tsx.snap index 7d6bb71bb8..5a4c451f8f 100644 --- a/src/test/components/__snapshots__/TrackPower.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackPower.test.tsx.snap @@ -2,7 +2,7 @@ exports[`TrackPower draws a dot that matches the snapshot 1`] = `
`; @@ -146,15 +146,15 @@ Array [ exports[`TrackPower matches the component snapshot 1`] = `
diff --git a/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap b/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap index c5adef80e4..1b29ea28cc 100644 --- a/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap @@ -2,22 +2,22 @@ exports[`TrackProcessCPU draws a dot that matches the snapshot 1`] = `
`; exports[`TrackProcessCPU has a tooltip that matches the snapshot 1`] = `
CPU: 50% @@ -56,12 +56,12 @@ Array [ Array [ "moveTo", 0, - 24, + 26, ], Array [ "lineTo", 15, - 17.866666666666667, + 18, ], Array [ "lineTo", @@ -76,27 +76,27 @@ Array [ Array [ "lineTo", 40, - 19.4, + 19, ], Array [ "lineTo", 50, - 12.5, + 13, ], Array [ "lineTo", 60, - 17.1, + 17, ], Array [ "lineTo", 70, - 21.7, + 22, ], Array [ "lineTo", 80, - 21.7, + 22, ], Array [ "stroke", @@ -119,17 +119,19 @@ Array [ exports[`TrackProcessCPU matches the component snapshot 1`] = `
- +
+ +
diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 428b559836..59d2016366 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -1488,7 +1488,7 @@ const DEFAULT_TEST_COUNTER_DISPLAY: CounterDisplayConfig = { unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 50, + sortWeight: 35, label: 'My Counter', }; diff --git a/src/test/fixtures/profiles/tracks.ts b/src/test/fixtures/profiles/tracks.ts index ae23c71340..88df189e41 100644 --- a/src/test/fixtures/profiles/tracks.ts +++ b/src/test/fixtures/profiles/tracks.ts @@ -88,22 +88,11 @@ export function getHumanReadableTracks(state: State): string[] { for (const trackIndex of trackOrder) { const track = tracks[trackIndex]; let trackName; - if (track.type === 'memory') { - trackName = profileViewSelectors + if (track.type === 'counter') { + const counter = profileViewSelectors .getCounterSelectors(track.counterIndex) - .getPid(state); - } else if (track.type === 'bandwidth') { - trackName = profileViewSelectors - .getCounterSelectors(track.counterIndex) - .getPid(state); - } else if (track.type === 'process-cpu') { - trackName = profileViewSelectors - .getCounterSelectors(track.counterIndex) - .getPid(state); - } else if (track.type === 'power') { - trackName = profileViewSelectors - .getCounterSelectors(track.counterIndex) - .getCounter(state).name; + .getCounter(state); + trackName = counter.display.label || counter.name; } else if (track.type === 'marker') { trackName = stringArray[track.markerName]; } else { @@ -297,6 +286,15 @@ export function getStoreWithMemoryTrack(pid: Pid = '222') { thread.pid = pid; const counter = getCounterForThread(thread, threadIndex); counter.category = 'Memory'; + counter.display = { + ...counter.display, + graphType: 'line-accumulated', + unit: 'bytes', + color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; profile.counters = [counter]; } @@ -306,8 +304,8 @@ export function getStoreWithMemoryTrack(pid: Pid = '222') { trackReference ); - if (localTrack.type !== 'memory') { - throw new Error('Expected a memory track.'); + if (localTrack.type !== 'counter') { + throw new Error('Expected a counter track.'); } return { store, ...store, profile, trackReference, localTrack, threadIndex }; } diff --git a/src/test/store/profile-view.test.ts b/src/test/store/profile-view.test.ts index c78779dcf3..4bac18e1a6 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -511,8 +511,8 @@ describe('actions/ProfileView', function () { store.getState(), memoryTrackReference ); - if (memoryTrack.type !== 'memory') { - throw new Error('Expected to get memory track.'); + if (memoryTrack.type !== 'counter') { + throw new Error('Expected to get counter track.'); } } diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index b634b0e876..cc641fbe78 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -634,12 +634,9 @@ export type GlobalTrack = export type LocalTrack = | { readonly type: 'thread'; readonly threadIndex: ThreadIndex } | { readonly type: 'network'; readonly threadIndex: ThreadIndex } - | { readonly type: 'memory'; readonly counterIndex: CounterIndex } - | { readonly type: 'bandwidth'; readonly counterIndex: CounterIndex } + | { readonly type: 'counter'; readonly counterIndex: CounterIndex } | { readonly type: 'ipc'; readonly threadIndex: ThreadIndex } | { readonly type: 'event-delay'; readonly threadIndex: ThreadIndex } - | { readonly type: 'process-cpu'; readonly counterIndex: CounterIndex } - | { readonly type: 'power'; readonly counterIndex: CounterIndex } | { readonly type: 'marker'; readonly threadIndex: ThreadIndex;