diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index c3d6daa277..7584c1f7be 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/app-logic/url-handling.ts b/src/app-logic/url-handling.ts index d8ab78b002..02b2907421 100644 --- a/src/app-logic/url-handling.ts +++ b/src/app-logic/url-handling.ts @@ -53,7 +53,7 @@ import { StringTable } from 'firefox-profiler/utils/string-table'; import type { ProfileUpgradeInfo } from 'firefox-profiler/profile-logic/processed-profile-versioning'; import type { ProfileAndProfileUpgradeInfo } from 'firefox-profiler/actions/receive-profile'; -export const CURRENT_URL_VERSION = 15; +export const CURRENT_URL_VERSION = 16; /** * This static piece of state might look like an anti-pattern, but it's a relatively @@ -1369,8 +1369,260 @@ const _upgraders: { .map(mapIndexesInTransform) .join('~'); }, + [16]: ( + processedLocation: ProcessedLocationBeforeUpgrade, + profile?: Profile + ) => { + // The 'memory', 'power', 'bandwidth', and 'process-cpu' LocalTrack types + // have been collapsed into a single 'counter' type. This moves counter + // tracks into a single group inside each PID's local track array, which + // shifts the track indexes that older URLs recorded. Remap + // localTrackOrderByPid and hiddenLocalTracksByPid so existing URLs keep + // pointing at the same tracks. + const { query } = processedLocation; + if (!profile || !profile.counters || profile.counters.length === 0) { + return; + } + if (!query.localTrackOrderByPid && !query.hiddenLocalTracksByPid) { + return; + } + + const oldToNewIndexByPid = _computeV16LocalTrackIndexRemap(profile); + + if (query.localTrackOrderByPid) { + query.localTrackOrderByPid = (query.localTrackOrderByPid as string) + .split('~') + .map((pidAndTracks) => { + const dash = pidAndTracks.indexOf('-'); + if (dash === -1) { + return pidAndTracks; + } + const pid = pidAndTracks.slice(0, dash); + const encoded = pidAndTracks.slice(dash + 1); + const remap = oldToNewIndexByPid.get(pid); + if (!remap) { + return pidAndTracks; + } + const oldOrder = decodeUintArrayFromUrlComponent(encoded); + const newOrder = []; + for (const oldIndex of oldOrder) { + const newIndex = remap[oldIndex]; + if (newIndex !== undefined && newIndex !== null) { + newOrder.push(newIndex); + } + } + return `${pid}-${encodeUintArrayForUrlComponent(newOrder)}`; + }) + .join('~'); + } + + if (query.hiddenLocalTracksByPid) { + query.hiddenLocalTracksByPid = (query.hiddenLocalTracksByPid as string) + .split('~') + .map((pidAndTracks) => { + const dash = pidAndTracks.indexOf('-'); + if (dash === -1) { + return pidAndTracks; + } + const pid = pidAndTracks.slice(0, dash); + const encoded = pidAndTracks.slice(dash + 1); + const remap = oldToNewIndexByPid.get(pid); + if (!remap) { + return pidAndTracks; + } + const oldHidden = decodeUintArrayFromUrlComponent(encoded); + const newHidden = new Set(); + for (const oldIndex of oldHidden) { + const newIndex = remap[oldIndex]; + if (newIndex !== undefined && newIndex !== null) { + newHidden.add(newIndex); + } + } + return `${pid}-${encodeUintSetForUrlComponent(newHidden)}`; + }) + .join('~'); + } + }, }; +/** + * Produce a per-PID mapping from old local-track indexes (using the pre-v16 + * LOCAL_TRACK_INDEX_ORDER) to new local-track indexes (post-v16). Used by the + * v16 URL upgrader. + * + * The two layouts share the same set of tracks; only the relative positions of + * counter tracks differ. The helper reconstructs both layouts by simulating + * what computeLocalTracksByPid would have produced under each. + */ +function _computeV16LocalTrackIndexRemap( + profile: Profile +): Map> { + // Pre-v16 LOCAL_TRACK_INDEX_ORDER. + // Note: we don't preserve 'event-delay' and 'process-cpu' in URLs, + // as they are experimental. + const OLD_SLOT = { + thread: 0, + network: 1, + memory: 2, + ipc: 3, + marker: 7, + power: 6, + bandwidth: 8, + }; + // Post-v16 LOCAL_TRACK_INDEX_ORDER. + const NEW_SLOT = { + thread: 0, + network: 1, + counter: 2, + ipc: 3, + marker: 5, + }; + + type Entry = { + id: string; + oldSlot: number | null; + newSlot: number | null; + }; + + const entriesByPid = new Map(); + const ensurePid = (pid: Pid): Entry[] => { + let entries = entriesByPid.get(pid); + if (entries === undefined) { + entries = []; + entriesByPid.set(pid, entries); + } + return entries; + }; + + const markerSchemasWithGraphs = (profile.meta.markerSchema || []).filter( + (schema) => Array.isArray(schema.graphs) && schema.graphs.length > 0 + ); + + for ( + let threadIndex = 0; + threadIndex < profile.threads.length; + threadIndex++ + ) { + const thread = profile.threads[threadIndex]; + const { pid, markers } = thread; + + if (!thread.isMainThread) { + ensurePid(pid).push({ + id: `t:${threadIndex}`, + oldSlot: OLD_SLOT.thread, + newSlot: NEW_SLOT.thread, + }); + } + if (markers.data.some((datum) => datum && datum.type === 'Network')) { + ensurePid(pid).push({ + id: `n:${threadIndex}`, + oldSlot: OLD_SLOT.network, + newSlot: NEW_SLOT.network, + }); + } + if (markers.data.some((datum) => datum && datum.type === 'IPC')) { + ensurePid(pid).push({ + id: `i:${threadIndex}`, + oldSlot: OLD_SLOT.ipc, + newSlot: NEW_SLOT.ipc, + }); + } + + if (markerSchemasWithGraphs.length > 0) { + const markerTracksBySchemaName: Map< + string, + { keys: string[]; markerNames: Set } + > = new Map(); + for (const markerSchema of markerSchemasWithGraphs) { + markerTracksBySchemaName.set(markerSchema.name, { + keys: (markerSchema.graphs || []).map((graph) => graph.key), + markerNames: new Set(), + }); + } + for (let i = 0; i < markers.length; ++i) { + const markerNameIndex = markers.name[i]; + const markerData = markers.data[i]; + if (markerData && markerData.type) { + const mapEntry = markerTracksBySchemaName.get(markerData.type); + if (mapEntry && mapEntry.keys.every((k) => k in markerData)) { + mapEntry.markerNames.add(markerNameIndex); + } + } + } + for (const [schemaName, { markerNames }] of markerTracksBySchemaName) { + for (const markerName of markerNames) { + ensurePid(pid).push({ + id: `m:${threadIndex}:${schemaName}:${markerName}`, + oldSlot: OLD_SLOT.marker, + newSlot: NEW_SLOT.marker, + }); + } + } + } + } + + const { counters } = profile; + if (counters) { + for (let counterIndex = 0; counterIndex < counters.length; counterIndex++) { + const counter = counters[counterIndex]; + const { pid, category, name, samples } = counter; + + // OLD behavior: only Memory / Bandwidth / Power produced tracks. + let oldSlot: number | null; + if (category === 'Memory') { + oldSlot = OLD_SLOT.memory; + } else if (category === 'Bandwidth') { + oldSlot = OLD_SLOT.bandwidth; + } + // We assumed there was no data when <= 2 samples. + else if (category === 'power' && samples.length > 2) { + oldSlot = OLD_SLOT.power; + } else { + oldSlot = null; + } + + // NEW behavior: mirror computeLocalTracksByPid. processCPU counters are + // added later by addProcessCPUTracksForProcess when the experimental + // toggle fires, every other counter becomes a track. + const newSlot: number | null = + category === 'CPU' && name === 'processCPU' ? null : NEW_SLOT.counter; + + if (oldSlot === null && newSlot === null) { + continue; + } + + ensurePid(pid).push({ + id: `c:${counterIndex}`, + oldSlot, + newSlot, + }); + } + } + + const remapByPid = new Map>(); + for (const [pid, entries] of entriesByPid) { + const oldList = entries + .filter((e) => e.oldSlot !== null) + .slice() + .sort((a, b) => (a.oldSlot as number) - (b.oldSlot as number)); + const newList = entries + .filter((e) => e.newSlot !== null) + .slice() + .sort((a, b) => (a.newSlot as number) - (b.newSlot as number)); + + const newIdToIndex = new Map(); + newList.forEach((entry, i) => newIdToIndex.set(entry.id, i)); + + const remap: Array = oldList.map((entry) => { + const newIndex = newIdToIndex.get(entry.id); + return newIndex === undefined ? null : newIndex; + }); + remapByPid.set(pid, remap); + } + + return remapByPid; +} + for (let destVersion = 1; destVersion <= CURRENT_URL_VERSION; destVersion++) { if (!_upgraders[destVersion]) { throw new Error(`There is no upgrader for version ${destVersion}.`); 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 ( +) { + const { markerSchemaLocation: _unused, ...rest } = props; + return <_SizedTimelineMarkers {...rest} />; +} + +export const TimelineMarkersCounter = explicitConnect< + TimelineMarkersCounterOwnProps, + StateProps, + DispatchProps +>({ + mapStateToProps: (state, props) => { + const { threadsKey, markerSchemaLocation } = props; + const selectors = getThreadSelectorsFromThreadsKey(threadsKey); + const selectedThreads = getSelectedThreadIndexes(state); + + return { + getMarker: selectors.getMarkerGetter(state), + markerIndexes: + selectors.getTimelineMarkerIndexesBySchemaLocation( + markerSchemaLocation + )(state), + isSelected: _getTimelineMarkersIsSelected(selectedThreads, threadsKey), + isModifyingSelection: getPreviewSelectionIsBeingModified(state), + additionalClassName: 'timelineMarkersCounter', + testId: 'TimelineMarkersCounter', + rightClickedMarker: selectors.getRightClickedMarker(state), + }; + }, + mapDispatchToProps: { changeRightClickedMarker }, + component: _TimelineMarkersCounterInner, +}); diff --git a/src/components/timeline/TrackBandwidth.css b/src/components/timeline/TrackBandwidth.css deleted file mode 100644 index 8d2d520faf..0000000000 --- a/src/components/timeline/TrackBandwidth.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/. */ - -.timelineTrackBandwidthGraph { - position: relative; - width: 100%; - height: var(--graph-height); -} - -.timelineTrackBandwidthCanvas { - position: absolute; - width: 100%; - height: 100%; -} - -.timelineTrackBandwidthGraphDot { - position: absolute; - width: 6px; - height: 6px; - border-radius: 3px; - margin-top: -3px; - margin-left: -3px; - pointer-events: none; -} diff --git a/src/components/timeline/TrackBandwidth.tsx b/src/components/timeline/TrackBandwidth.tsx deleted file mode 100644 index 3935008d35..0000000000 --- a/src/components/timeline/TrackBandwidth.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 { TrackBandwidthGraph } from './TrackBandwidthGraph'; -import { - TRACK_BANDWIDTH_HEIGHT, - TRACK_BANDWIDTH_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 './TrackBandwidth.css'; - -type OwnProps = { - readonly counterIndex: CounterIndex; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; -}; - -type DispatchProps = {}; - -type Props = ConnectedProps; - -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/TrackContextMenu.tsx b/src/components/timeline/TrackContextMenu.tsx index d4939d3526..eb17df155e 100644 --- a/src/components/timeline/TrackContextMenu.tsx +++ b/src/components/timeline/TrackContextMenu.tsx @@ -936,7 +936,7 @@ class TimelineTrackContextMenuImpl extends PureComponent< const ALLOWED_TYPES = [ 'screenshots', - 'memory', + 'counter', 'network', 'ipc', 'event-delay', diff --git a/src/components/timeline/TrackMemory.css b/src/components/timeline/TrackCounter.css similarity index 74% rename from src/components/timeline/TrackMemory.css rename to src/components/timeline/TrackCounter.css index 404b625b0c..f76deda599 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,19 @@ 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; } +.timelineMarkersCounter, .timelineMarkersMemory { - height: var(--markers-height, 15px); + height: var(--markers-height); opacity: 1; } diff --git a/src/components/timeline/TrackProcessCPU.tsx b/src/components/timeline/TrackCounter.tsx similarity index 52% rename from src/components/timeline/TrackProcessCPU.tsx rename to src/components/timeline/TrackCounter.tsx index 44b6eb15b6..effc64db1d 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 { TimelineMarkersCounter } 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, + MarkerDisplayLocation, } 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 markerSchemaLocation: MarkerDisplayLocation | null; }; type DispatchProps = { @@ -41,32 +45,55 @@ 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, + markerSchemaLocation, + } = this.props; + return (
- + ) : null} +
); } } -export const TrackProcessCPU = explicitConnect< +export const TrackCounter = explicitConnect< OwnProps, StateProps, DispatchProps @@ -80,8 +107,9 @@ export const TrackProcessCPU = explicitConnect< threadIndex: counter.mainThreadIndex, rangeStart: start, rangeEnd: end, + markerSchemaLocation: counter.display.markerSchemaLocation, }; }, 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..a90c36eb39 --- /dev/null +++ b/src/components/timeline/TrackCounterGraph.tsx @@ -0,0 +1,901 @@ +/* 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 { assertExhaustiveCheck } from 'firefox-profiler/utils/types'; +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(); + + switch (display.graphType) { + case '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(); + break; + } + case 'line-rate': { + // 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 ( + 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(); + break; + } + default: + throw assertExhaustiveCheck(display.graphType); + } + } + } + + _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; + + switch (display.graphType) { + case 'line-accumulated': { + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + const unitSampleCount = + (accumulatedCounts[counterIndex] - minCount) / countRange; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + break; + } + case 'line-rate': { + 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; + break; + } + default: + throw assertExhaustiveCheck(display.graphType); + } + + 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..709b3c048e 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -980,7 +980,16 @@ function _deriveCounterDisplay( category: string, name: string ): CounterDisplayConfig { - if (category === 'Memory') { + if (category === 'Bandwidth') { + return { + graphType: 'line-rate', + unit: 'bytes', + color: 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'Memory') { return { graphType: 'line-accumulated', unit: 'bytes', @@ -998,15 +1007,6 @@ function _deriveCounterDisplay( sortWeight: 30, label: name, }; - } else if (category === 'Bandwidth') { - return { - graphType: 'line-rate', - unit: 'bytes', - color: 'blue', - markerSchemaLocation: null, - sortWeight: 10, - label: 'Bandwidth', - }; } else if (category === 'CPU' && name === 'processCPU') { return { graphType: 'line-rate', diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index c8d94509d9..725b597997 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3059,7 +3059,16 @@ const _upgraders: { continue; } const { category, name } = counter; - if (category === 'Memory') { + if (category === 'Bandwidth') { + counter.display = { + graphType: 'line-rate', + unit: 'bytes', + color: counter.color ?? 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'Memory') { counter.display = { graphType: 'line-accumulated', unit: 'bytes', @@ -3077,15 +3086,6 @@ const _upgraders: { sortWeight: 30, label: name, }; - } else if (category === 'Bandwidth') { - counter.display = { - graphType: 'line-rate', - unit: 'bytes', - color: counter.color ?? 'blue', - markerSchemaLocation: null, - sortWeight: 10, - label: 'Bandwidth', - }; } else if (category === 'CPU' && name === 'processCPU') { counter.display = { graphType: 'line-rate', diff --git a/src/profile-logic/tracks.ts b/src/profile-logic/tracks.ts index c831ad796e..ea5c690bbb 100644 --- a/src/profile-logic/tracks.ts +++ b/src/profile-logic/tracks.ts @@ -56,28 +56,22 @@ export type HiddenTracks = { const LOCAL_TRACK_INDEX_ORDER = { thread: 0, network: 1, - memory: 2, + counter: 2, ipc: 3, 'event-delay': 4, - 'process-cpu': 5, - power: 6, - marker: 7, - bandwidth: 8, + marker: 5, }; const LOCAL_TRACK_DISPLAY_ORDER = { network: 0, - bandwidth: 1, - memory: 2, - power: 3, + 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: 2, + thread: 3, + 'event-delay': 4, + marker: 5, }; const GLOBAL_TRACK_INDEX_ORDER = { @@ -103,6 +97,27 @@ function _getDefaultLocalTrackOrder( const naturalSort = new Intl.Collator('en-US', { numeric: true }); // In place sort! trackOrder.sort((a, b) => { + // Tie-break between two counter tracks using their display.sortWeight. + // Cross-type ordering is handled below by LOCAL_TRACK_DISPLAY_ORDER. + if ( + tracks[a].type === 'counter' && + tracks[b].type === 'counter' && + profile && + profile.counters + ) { + if (profile.meta.keepProfileThreadOrder) { + return tracks[a].counterIndex - tracks[b].counterIndex; + } + const counterA = profile.counters[tracks[a].counterIndex]; + const counterB = profile.counters[tracks[b].counterIndex]; + const sortWeightDiff = + counterA.display.sortWeight - counterB.display.sortWeight; + if (sortWeightDiff !== 0) { + return sortWeightDiff; + } + return naturalSort.compare(counterA.name, counterB.name); + } + if ( tracks[a].type === 'thread' && tracks[b].type === 'ipc' && @@ -123,22 +138,6 @@ function _getDefaultLocalTrackOrder( return 1; } - if ( - profile && - profile.counters && - tracks[a].type === 'power' && - tracks[b].type === 'power' - ) { - const idxA = tracks[a].counterIndex; - const idxB = tracks[b].counterIndex; - if (profile.meta.keepProfileThreadOrder) { - return idxA - idxB; - } - const nameA = profile.counters[idxA].name; - const nameB = profile.counters[idxB].name; - return naturalSort.compare(nameA, nameB); - } - // If the tracks are both threads, sort them by thread name, and then by // creation time if they have the same name. if (tracks[a].type === 'thread' && tracks[b].type === 'thread' && profile) { @@ -400,30 +399,24 @@ 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 } = 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; + } + + let tracks = localTracksByPid.get(pid); + if (tracks === undefined) { + tracks = []; + localTracksByPid.set(pid, tracks); } + tracks.push({ type: 'counter', counterIndex }); } } @@ -496,7 +489,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 +1109,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 +1123,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 +1566,18 @@ export function getSearchFilteredLocalTracksByPid( } break; } + case '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); @@ -1762,7 +1755,8 @@ export function getTrackReferenceFromThreadIndex( * 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. + * TODO: Check the counter track activity here to decide if it should be visible, + * see https://github.com/firefox-devtools/profiler/issues/5967. */ function _isLocalTrackVisible( localTrack: LocalTrack, @@ -1774,15 +1768,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..c477985be5 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,22 @@ 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 the counter asks for + // markers to be rendered above the graph. + const counter = ensureExists(counters)[localTrack.counterIndex]; + height += counter.display.markerSchemaLocation + ? 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/per-thread/markers.ts b/src/selectors/per-thread/markers.ts index 8f083a15a7..27d5b86483 100644 --- a/src/selectors/per-thread/markers.ts +++ b/src/selectors/per-thread/markers.ts @@ -21,6 +21,7 @@ import type { MarkerIndex, Marker, MarkerSchema, + MarkerDisplayLocation, MarkerTiming, MarkerTimingAndBuckets, DerivedMarkerInfo, @@ -492,29 +493,40 @@ export function getMarkerSelectorsPerThread( ); /** - * This returns only memory markers. - */ - const getTimelineMemoryMarkerIndexes: Selector = - createSelector( - getMarkerGetter, - getCommittedRangeFilteredMarkerIndexes, - ProfileSelectors.getMarkerSchema, - ProfileSelectors.getMarkerSchemaByName, - () => 'timeline-memory' as const, - MarkerData.filterMarkerByDisplayLocation - ); + * Returns markers for an arbitrary schema location. The inner selectors are + * memoized per location so repeated lookups with the same location reuse a + * single reselect instance. + */ + const _timelineMarkerIndexesSelectorsBySchemaLocation: Map< + MarkerDisplayLocation, + Selector + > = new Map(); + const getTimelineMarkerIndexesBySchemaLocation = ( + schemaLocation: MarkerDisplayLocation + ): Selector => { + let selector = + _timelineMarkerIndexesSelectorsBySchemaLocation.get(schemaLocation); + if (selector === undefined) { + selector = createSelector( + getMarkerGetter, + getCommittedRangeFilteredMarkerIndexes, + ProfileSelectors.getMarkerSchema, + ProfileSelectors.getMarkerSchemaByName, + () => schemaLocation, + MarkerData.filterMarkerByDisplayLocation + ); + _timelineMarkerIndexesSelectorsBySchemaLocation.set( + schemaLocation, + selector + ); + } + return selector; + }; - /** - * This returns only IPC markers. - */ - const getTimelineIPCMarkerIndexes: Selector = createSelector( - getMarkerGetter, - getCommittedRangeFilteredMarkerIndexes, - ProfileSelectors.getMarkerSchema, - ProfileSelectors.getMarkerSchemaByName, - () => 'timeline-ipc' as const, - MarkerData.filterMarkerByDisplayLocation - ); + const getTimelineMemoryMarkerIndexes = + getTimelineMarkerIndexesBySchemaLocation('timeline-memory'); + const getTimelineIPCMarkerIndexes = + getTimelineMarkerIndexesBySchemaLocation('timeline-ipc'); /** * This organizes the network markers in rows so that they're nicely displayed @@ -783,6 +795,7 @@ export function getMarkerSelectorsPerThread( getTimelineFileIoMarkerIndexes, getTimelineMemoryMarkerIndexes, getTimelineIPCMarkerIndexes, + getTimelineMarkerIndexesBySchemaLocation, getNetworkTrackTiming, getRangeFilteredScreenshotsById, getSearchFilteredMarkerIndexes, diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 4661385abd..331b27ed6f 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -555,10 +555,17 @@ 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' && + ensureExists(counters)[track.counterIndex].category === '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..6247499fde 100644 --- a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap @@ -27,12 +27,12 @@ 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..0af9de7c48 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`] = `
`; @@ -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..77575f665c 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,12 +182,12 @@ 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..d0285e6d0f 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`] = `
`; @@ -68,22 +68,22 @@ Array [ Array [ "lineTo", 15, - 24, + 23.877333333333333, ], Array [ "lineTo", 20, - 24, + 23.54, ], Array [ "lineTo", 30, - 24, + 23.54, ], Array [ "lineTo", 40, - 15, + 14.799999999999999, ], Array [ "lineTo", @@ -93,37 +93,37 @@ Array [ Array [ "lineTo", 60, - 24, + 23.862, ], Array [ "lineTo", 71, - 20, + 19.700934579439256, ], Array [ "lineTo", 71, - 24, + 23.99954, ], Array [ "lineTo", 71, - 15, + 14.799999999999804, ], Array [ "lineTo", 71, - 18, + 18.479999999999883, ], Array [ "lineTo", 110, - 24, + 23.882051282051282, ], Array [ "lineTo", 120, - 24, + 23.882051282051282, ], Array [ "stroke", @@ -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..a5cbda9940 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,7 +56,7 @@ Array [ Array [ "moveTo", 0, - 24, + 26, ], Array [ "lineTo", @@ -119,17 +119,19 @@ Array [ exports[`TrackProcessCPU matches the component snapshot 1`] = `
- +
+ +
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 7ca08b5fbb..2b830e08c5 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -512,8 +512,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/test/url-handling.test.ts b/src/test/url-handling.test.ts index ae1a987eb9..30abd56b90 100644 --- a/src/test/url-handling.test.ts +++ b/src/test/url-handling.test.ts @@ -52,6 +52,7 @@ import { import { getProfileFromTextSamples, getProfileWithMarkers, + getCounterForThread, } from './fixtures/profiles/processed-profile'; import { selectedThreadSelectors } from '../selectors/per-thread'; import { @@ -1279,6 +1280,142 @@ describe('url upgrading', function () { }); }); + describe('version 16: collapse counter track types into a single counter type', function () { + // Build a profile whose pid '222' has one main thread with an IPC marker and + // three counters (Memory, power, Bandwidth). This exercises the v16 upgrader: + // under the pre-v16 LOCAL_TRACK_INDEX_ORDER the ipc track sat between + // 'memory' (slot 2) and 'power' (slot 6), which no longer holds now that all + // counters share slot 2. + function buildCounterProfile() { + const { profile } = getProfileFromTextSamples('A B C D E'); + const mainThread = profile.threads[0]; + mainThread.pid = '222'; + mainThread.name = 'GeckoMain'; + mainThread.isMainThread = true; + mainThread.processType = 'tab'; + + // Push a single IPC marker onto the main thread. The upgrader only + // checks markers.data[i].type === 'IPC', so a minimal payload is enough. + const stringTable = StringTable.withBackingArray( + profile.shared.stringArray + ); + mainThread.markers.name.push(stringTable.indexForString('IPC')); + mainThread.markers.phase.push(0); + mainThread.markers.startTime.push(0); + mainThread.markers.endTime.push(null); + mainThread.markers.category.push(0); + mainThread.markers.data.push({ + type: 'IPC', + startTime: 0, + endTime: 1, + otherPid: '333', + messageSeqno: 1, + messageType: 'Foo', + side: 'parent', + direction: 'sending', + phase: 'endpoint', + sync: false, + } as any); + mainThread.markers.length++; + + const memoryCounter = getCounterForThread(mainThread, 0); + memoryCounter.category = 'Memory'; + memoryCounter.name = 'Memory'; + const powerCounter = getCounterForThread(mainThread, 0); + powerCounter.category = 'power'; + powerCounter.name = 'Power'; + const bandwidthCounter = getCounterForThread(mainThread, 0); + bandwidthCounter.category = 'Bandwidth'; + bandwidthCounter.name = 'Bandwidth'; + profile.counters = [memoryCounter, powerCounter, bandwidthCounter]; + + return profile; + } + + it('remaps localTrackOrderByPid when counter tracks change position', function () { + const profile = buildCounterProfile(); + + // Pre-v16 layout for pid '222' (sorted by old LOCAL_TRACK_INDEX_ORDER): + // [0] memory (slot 2) + // [1] ipc (slot 3) + // [2] power (slot 6) + // [3] bandwidth (slot 8) + const oldOrder = [3, 2, 1, 0]; + + // Post-v16 layout: counters grouped at slot 2, ipc at slot 3. + // [0] memory (counter, insertion order) + // [1] power (counter) + // [2] bandwidth (counter) + // [3] ipc + const expectedNewOrder = [2, 1, 3, 0]; + + const { query } = upgradeLocationToCurrentVersion( + { + pathname: '', + hash: '', + query: { + v: '15', + localTrackOrderByPid: + '222-' + encodeUintArrayForUrlComponent(oldOrder), + }, + }, + profile + ); + + expect(query.localTrackOrderByPid).toBe( + '222-' + encodeUintArrayForUrlComponent(expectedNewOrder) + ); + }); + + it('remaps hiddenLocalTracksByPid when counter tracks change position', function () { + const profile = buildCounterProfile(); + + // Hide the old 'ipc' (index 1) and old 'bandwidth' (index 3). + const oldHidden = new Set([1, 3]); + // In the new layout those tracks live at indexes 3 (ipc) and 2 (bandwidth). + const expectedNewHidden = new Set([3, 2]); + + const { query } = upgradeLocationToCurrentVersion( + { + pathname: '', + hash: '', + query: { + v: '15', + hiddenLocalTracksByPid: + '222-' + encodeUintSetForUrlComponent(oldHidden), + }, + }, + profile + ); + + expect(query.hiddenLocalTracksByPid).toBe( + '222-' + encodeUintSetForUrlComponent(expectedNewHidden) + ); + }); + + it('leaves unrelated PIDs untouched', function () { + const profile = buildCounterProfile(); + + // pid '999' isn't in the profile, so the upgrader has nothing to remap + // for it and should leave the segment as-is. + const untouched = '999-' + encodeUintArrayForUrlComponent([1, 0]); + + const { query } = upgradeLocationToCurrentVersion( + { + pathname: '', + hash: '', + query: { + v: '15', + localTrackOrderByPid: untouched, + }, + }, + profile + ); + + expect(query.localTrackOrderByPid).toBe(untouched); + }); + }); + // More general checks it("won't run if the current version is specified", function () { const { getState } = _getStoreWithURL({ 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; diff --git a/src/types/profile.ts b/src/types/profile.ts index a4072ca72e..04d8a2ee79 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -3,7 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { Milliseconds, Address, Microseconds, Bytes } from './units'; -import type { MarkerPayload, MarkerSchema, MarkerFormatType } from './markers'; +import type { + MarkerPayload, + MarkerSchema, + MarkerFormatType, + MarkerDisplayLocation, +} from './markers'; import type { MarkerPhase, ProfilingLog } from './gecko-profile'; export type IndexIntoStackTable = number; @@ -540,7 +545,7 @@ export type CounterDisplayConfig = { color: GraphColor; // The marker schema display location to filter markers for this track, // e.g., "timeline-memory". If null, no markers are shown. - markerSchemaLocation: string | null; + markerSchemaLocation: MarkerDisplayLocation | null; // Controls the default display position of this counter track relative to // other tracks. Tracks with lower values appear closer to the top. sortWeight: number;