From 9d18c91af69d74cf945b0153ecb9d200e98bd9c2 Mon Sep 17 00:00:00 2001 From: fatadel Date: Wed, 13 May 2026 19:17:23 +0200 Subject: [PATCH] Drive counter tooltips from a tooltipRows schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the four hardcoded category branches in TrackCounterGraph's tooltip rendering (Memory / Power / Bandwidth / processCPU) with a declarative tooltipRows array on CounterDisplayConfig. Each row picks a data source (count, rate, accumulated, selection-total, …), a value format (unit, optional CO₂e, optional auto-scale ladder), and a label. The new TrackCounterTooltip component iterates the rows, resolves each source, formats the value, and renders through TooltipDetails. Memory and CPU tooltips now use the same TooltipDetails layout as Power and Bandwidth. Profile labels are raw English so the format stays self-describing. The frontend translates labels it recognizes from a private allow-list and renders unknown ones verbatim. The dedicated TooltipTrackPower component is removed; the power/energy unit ladders move into the formatter. Bumps the processed profile format to v63. The v63 upgrader derives tooltipRows from each counter's category and name. Closes #5961. --- docs-developer/CHANGELOG-formats.md | 6 + locales/en-US/app.ftl | 25 +- src/app-logic/constants.ts | 2 +- src/components/timeline/TrackCounterGraph.tsx | 243 +----------- .../timeline/TrackCounterTooltip.tsx | 370 ++++++++++++++++++ .../timeline/TrackCounterTooltipFormat.ts | 177 +++++++++ src/components/tooltip/TrackPower.tsx | 218 ----------- src/profile-logic/process-profile.ts | 115 +++++- .../processed-profile-versioning.ts | 131 +++++++ src/test/components/TrackMemory.test.tsx | 15 +- src/test/components/TrackPower.test.tsx | 2 +- src/test/components/TrackProcessCPU.test.tsx | 2 +- .../__snapshots__/TrackMemory.test.tsx.snap | 71 ++-- .../__snapshots__/TrackPower.test.tsx.snap | 2 +- .../TrackProcessCPU.test.tsx.snap | 14 +- .../fixtures/profiles/processed-profile.ts | 32 +- src/test/fixtures/profiles/tracks.ts | 14 +- .../__snapshots__/profiler-edit.test.ts.snap | 8 +- .../__snapshots__/profile-view.test.ts.snap | 2 +- .../profile-conversion.test.ts.snap | 36 +- .../profile-upgrading.test.ts.snap | 37 +- src/types/profile.ts | 57 +++ 22 files changed, 1006 insertions(+), 573 deletions(-) create mode 100644 src/components/timeline/TrackCounterTooltip.tsx create mode 100644 src/components/timeline/TrackCounterTooltipFormat.ts delete mode 100644 src/components/tooltip/TrackPower.tsx diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index 50da5c96d4..22dfbade6a 100644 --- a/docs-developer/CHANGELOG-formats.md +++ b/docs-developer/CHANGELOG-formats.md @@ -6,6 +6,12 @@ Note that this is not an exhaustive list. Processed profile format upgraders can ## Processed profile format +### Version 63 + +A new `tooltipRows` field was added to `CounterDisplayConfig`. +This metadata describes the rows of the counter's hover tooltip (data source, value format, label). +For existing profiles, the rows are derived from the counter's `category` and `name`. + ### Version 62 A new `display` field of type `CounterDisplayConfig` was added to `RawCounter`. diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index be577ee3a9..2af23b424a 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -964,9 +964,28 @@ TrackNameButton--hide-process = ## the UI. To learn more about it, visit: ## https://profiler.firefox.com/docs/#/./memory-allocations?id=memory-track -TrackMemoryGraph--relative-memory-at-this-time = relative memory at this time -TrackMemoryGraph--memory-range-in-graph = memory range in graph -TrackMemoryGraph--allocations-and-deallocations-since-the-previous-sample = allocations and deallocations since the previous sample +# Variables: +# $value (String) - the relative memory at this time (e.g. "5MB") +TrackMemoryGraph--relative-memory-at-this-time2 = { $value } + .label = relative memory at this time + +# Variables: +# $value (String) - the memory range across the graph (e.g. "5MB") +TrackMemoryGraph--memory-range-in-graph2 = { $value } + .label = memory range in graph + +# Variables: +# $value (String) - count of allocations and deallocations since the previous sample +TrackMemoryGraph--allocations-and-deallocations-since-the-previous-sample2 = { $value } + .label = allocations and deallocations since the previous sample + +## TrackProcessCPUGraph +## This is used to show the CPU usage of a process over time in the timeline. + +# Variables: +# $value (String) - the CPU usage at this sample (e.g. "50%") +TrackProcessCPUGraph--cpu = { $value } + .label = CPU ## TrackPower ## This is used to show the power used by the CPU and other chips in a computer, diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index a8bdb0e23c..ef712c02b8 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 34; // The current version of the "processed" profile format. // Please don't forget to update the processed profile format changelog in // `docs-developer/CHANGELOG-formats.md`. -export const PROCESSED_PROFILE_VERSION = 62; +export const PROCESSED_PROFILE_VERSION = 63; // The following are the margin sizes for the left and right of the timeline. Independent // components need to share these values. diff --git a/src/components/timeline/TrackCounterGraph.tsx b/src/components/timeline/TrackCounterGraph.tsx index a90c36eb39..b0e4123163 100644 --- a/src/components/timeline/TrackCounterGraph.tsx +++ b/src/components/timeline/TrackCounterGraph.tsx @@ -4,7 +4,6 @@ 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, @@ -12,30 +11,16 @@ import { 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 { TrackCounterTooltip } from './TrackCounterTooltip'; 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, @@ -44,7 +29,6 @@ import type { ThreadIndex, AccumulatedCounterSamples, Milliseconds, - PreviewSelection, CssPixels, StartEndRange, IndexIntoSamplesTable, @@ -379,7 +363,6 @@ type StateProps = { readonly interval: Milliseconds; readonly filteredThread: Thread; readonly unfilteredSamplesRange: StartEndRange | null; - readonly previewSelection: PreviewSelection | null; }; type DispatchProps = {}; @@ -393,10 +376,8 @@ type State = { }; /** - * 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. + * The generic counter track graph component. It renders any counter + * (Memory, Power, etc.) as a graph in the timeline. */ class TrackCounterGraphImpl extends React.PureComponent { override state = { @@ -405,8 +386,6 @@ class TrackCounterGraphImpl extends React.PureComponent { 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. @@ -509,44 +488,14 @@ class TrackCounterGraphImpl extends React.PureComponent { } }; - _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, + accumulatedSamples, maxCounterSampleCountPerMs, - previewSelection, } = this.props; - const { display } = counter; const { mouseX, mouseY } = this.state; const { samples } = counter; @@ -563,182 +512,15 @@ class TrackCounterGraphImpl extends React.PureComponent { 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} -
-
-
+ ); } @@ -894,7 +676,6 @@ export const TrackCounterGraph = explicitConnect< interval: getProfileInterval(state), filteredThread: selectors.getFilteredThread(state), unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - previewSelection: getPreviewSelection(state), }; }, component: withSize(TrackCounterGraphImpl), diff --git a/src/components/timeline/TrackCounterTooltip.tsx b/src/components/timeline/TrackCounterTooltip.tsx new file mode 100644 index 0000000000..39b9efbc1a --- /dev/null +++ b/src/components/timeline/TrackCounterTooltip.tsx @@ -0,0 +1,370 @@ +/* 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 { Localized } from '@fluent/react'; + +import explicitConnect from 'firefox-profiler/utils/connect'; +import { + formatBytes, + formatNumber, + formatPercent, +} from 'firefox-profiler/utils/format-numbers'; +import { + getCommittedRange, + getPreviewSelection, + getProfileInterval, + getMeta, +} from 'firefox-profiler/selectors/profile'; +import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; +import { + TooltipDetails, + TooltipDetail, + TooltipDetailSeparator, +} from 'firefox-profiler/components/tooltip/TooltipDetails'; +import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; +import { assertExhaustiveCheck } from 'firefox-profiler/utils/types'; +import { + L10N_ID_BY_LABEL_KEY, + POWER_LADDER, + ENERGY_LADDER, + carbonForBytes, + carbonForWattHours, + pickTier, +} from './TrackCounterTooltipFormat'; + +import type { + AccumulatedCounterSamples, + Counter, + CounterTooltipDataSource, + CounterTooltipFormat, + CounterTooltipRow, + CssPixels, + Milliseconds, + PreviewSelection, + ProfileMeta, + StartEndRange, + State, +} from 'firefox-profiler/types'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +type ResolverContext = { + counter: Counter; + counterIndex: number; + interval: Milliseconds; + accumulatedSamples: AccumulatedCounterSamples; + maxCounterSampleCountPerMs: number; + committedRange: StartEndRange; + previewSelection: PreviewSelection | null; + meta: ProfileMeta; +}; + +function resolveSource( + source: CounterTooltipDataSource, + ctx: ResolverContext +): number | null { + const { counter, counterIndex, interval } = ctx; + const { samples } = counter; + + switch (source) { + case 'count': + return samples.count[counterIndex]; + case 'rate': { + const dt = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + return samples.count[counterIndex] / dt; + } + case 'cpu-ratio': { + const dt = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + return samples.count[counterIndex] / dt / ctx.maxCounterSampleCountPerMs; + } + case 'accumulated': { + const { minCount, accumulatedCounts } = ctx.accumulatedSamples; + return accumulatedCounts[counterIndex] - minCount; + } + case 'count-range': + return ctx.accumulatedSamples.countRange; + case 'sample-number': + return samples.number !== undefined ? samples.number[counterIndex] : null; + case 'selection-total': { + if (!ctx.previewSelection) { + return null; + } + const [begin, end] = getSampleIndexRangeForSelection( + samples, + ctx.previewSelection.selectionStart, + ctx.previewSelection.selectionEnd + ); + let sum = 0; + for (let i = begin; i < end; i++) { + sum += samples.count[i]; + } + return sum; + } + case 'selection-rate': { + if (!ctx.previewSelection) { + return null; + } + const span = + ctx.previewSelection.selectionEnd - ctx.previewSelection.selectionStart; + if (span <= 0) { + return null; + } + const [begin, end] = getSampleIndexRangeForSelection( + samples, + ctx.previewSelection.selectionStart, + ctx.previewSelection.selectionEnd + ); + let sum = 0; + for (let i = begin; i < end; i++) { + sum += samples.count[i]; + } + return sum / span; + } + case 'committed-range-total': { + const [begin, end] = getSampleIndexRangeForSelection( + samples, + ctx.committedRange.start, + ctx.committedRange.end + ); + let sum = 0; + for (let i = begin; i < end; i++) { + sum += samples.count[i]; + } + return sum; + } + default: + throw assertExhaustiveCheck(source); + } +} + +function formatValueRow( + value: number, + format: CounterTooltipFormat, + source: CounterTooltipDataSource, + label: string, + labelKey: string | undefined, + key: number, + ctx: ResolverContext +): React.ReactElement { + // Normalize the value into the ladder's input unit (watts for power, + // watt-hours for energy). samples.count[i] is energy in pWh accumulated + // over the sample's dt; selection-rate is pWh per ms; the range totals + // are sums of pWh. + let valueForLadder = value; + if (format.scale === 'power') { + if (source === 'count') { + const dt = + ctx.counterIndex === 0 + ? ctx.interval + : ctx.counter.samples.time[ctx.counterIndex] - + ctx.counter.samples.time[ctx.counterIndex - 1]; + valueForLadder = ((value * 1e-12) / dt) * 1000 * 3600; + } else if (source === 'selection-rate') { + valueForLadder = value * 1e-12 * 1000 * 3600; + } + } else if (format.scale === 'energy') { + valueForLadder = value * 1e-12; + } + + const knownL10nId = labelKey ? L10N_ID_BY_LABEL_KEY[labelKey] : undefined; + + if (format.scale) { + const ladder = format.scale === 'power' ? POWER_LADDER : ENERGY_LADDER; + const tier = pickTier(valueForLadder, ladder); + let carbonGrams = 0; + if (format.co2 === 'per-watthour' && format.scale === 'energy') { + carbonGrams = carbonForWattHours(valueForLadder, ctx.meta); + } + + const formattedValue = formatNumber( + valueForLadder * tier.multiplier, + tier.valueSignificantDigits + ); + const formattedCarbon = formatNumber( + carbonGrams * tier.carbonMultiplier, + tier.carbonSignificantDigits + ); + + if (knownL10nId) { + const vars: { value: string; carbonValue?: string } = { + value: formattedValue, + }; + if (format.co2) { + vars.carbonValue = formattedCarbon; + } + return ( + + {formattedValue} + + ); + } + + const valueWithUnit = `${formattedValue} ${tier.unitText}`; + return ( + + {format.scale === 'energy' && format.co2 === 'per-watthour' + ? `${valueWithUnit} (${formattedCarbon} ${tier.carbonUnitText})` + : valueWithUnit} + + ); + } + + let formattedValue: string; + switch (format.unit) { + case 'bytes': + formattedValue = formatBytes(value); + break; + case 'bytes-per-second': + // Input arrives in bytes/ms. + formattedValue = formatBytes(value * 1000); + break; + case 'percent': + formattedValue = formatPercent(value); + break; + case 'number': + formattedValue = formatNumber(value, 2, 0); + break; + default: + throw assertExhaustiveCheck(format.unit); + } + + let formattedCarbon: string | undefined; + if (format.co2 === 'per-byte') { + const bytesForCarbon = + format.unit === 'bytes-per-second' ? value * 1000 : value; + formattedCarbon = formatNumber(carbonForBytes(bytesForCarbon)); + } + + if (knownL10nId) { + const vars: { value: string; carbonValue?: string } = { + value: formattedValue, + }; + if (formattedCarbon !== undefined) { + vars.carbonValue = formattedCarbon; + } + return ( + + {formattedValue} + + ); + } + + let displayValue = formattedValue; + if (format.unit === 'bytes-per-second') { + displayValue += ' per second'; + } + if (formattedCarbon !== undefined) { + displayValue += ` (${formattedCarbon} g CO₂e)`; + } + return ( + + {displayValue} + + ); +} + +type OwnProps = { + readonly counter: Counter; + readonly counterIndex: number; + readonly accumulatedSamples: AccumulatedCounterSamples; + readonly maxCounterSampleCountPerMs: number; + readonly mouseX: CssPixels; + readonly mouseY: CssPixels; +}; + +type StateProps = { + readonly interval: Milliseconds; + readonly committedRange: StartEndRange; + readonly previewSelection: PreviewSelection | null; + readonly meta: ProfileMeta; +}; + +type Props = ConnectedProps; + +class TrackCounterTooltipImpl extends React.PureComponent { + override render() { + const { + counter, + counterIndex, + accumulatedSamples, + maxCounterSampleCountPerMs, + mouseX, + mouseY, + interval, + committedRange, + previewSelection, + meta, + } = this.props; + + const ctx: ResolverContext = { + counter, + counterIndex, + interval, + accumulatedSamples, + maxCounterSampleCountPerMs, + committedRange, + previewSelection, + meta, + }; + + const hasNonEmptySelection = + previewSelection !== null && + previewSelection.selectionEnd > previewSelection.selectionStart; + + const rendered: React.ReactNode[] = []; + counter.display.tooltipRows.forEach((row: CounterTooltipRow, i: number) => { + if (row.type === 'separator') { + rendered.push(); + return; + } + if (row.requiresPreviewSelection && !hasNonEmptySelection) { + return; + } + const value = resolveSource(row.source, ctx); + if (value === null) { + return; + } + rendered.push( + formatValueRow( + value, + row.format, + row.source, + row.label, + row.labelKey, + i, + ctx + ) + ); + }); + + return ( + +
+ {rendered} +
+
+ ); + } +} + +export const TrackCounterTooltip = explicitConnect({ + mapStateToProps: (state: State) => ({ + interval: getProfileInterval(state), + committedRange: getCommittedRange(state), + previewSelection: getPreviewSelection(state), + meta: getMeta(state), + }), + component: TrackCounterTooltipImpl, +}); diff --git a/src/components/timeline/TrackCounterTooltipFormat.ts b/src/components/timeline/TrackCounterTooltipFormat.ts new file mode 100644 index 0000000000..fbd36aea5c --- /dev/null +++ b/src/components/timeline/TrackCounterTooltipFormat.ts @@ -0,0 +1,177 @@ +/* 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 { co2, averageIntensity } from '@tgwf/co2'; + +import type { ProfileMeta } from 'firefox-profiler/types'; + +/** + * Maps recognized labelKey values to their Fluent message IDs. Rows with no + * labelKey, or a labelKey not listed here, render their raw English label + * verbatim. For auto-scaled rows, the value is a Fluent ID prefix; the ladder + * tier's suffix is appended at render time. + */ +export const L10N_ID_BY_LABEL_KEY: { [labelKey: string]: string } = { + // Memory + 'memory-relative': 'TrackMemoryGraph--relative-memory-at-this-time', + 'memory-range': 'TrackMemoryGraph--memory-range-in-graph', + 'memory-operations': + 'TrackMemoryGraph--allocations-and-deallocations-since-the-previous-sample', + // Process CPU + cpu: 'TrackProcessCPUGraph--cpu', + // Bandwidth + 'bandwidth-speed': 'TrackBandwidthGraph--speed', + 'bandwidth-operations': + 'TrackBandwidthGraph--read-write-operations-since-the-previous-sample', + 'bandwidth-cumulative': + 'TrackBandwidthGraph--cumulative-bandwidth-at-this-time', + 'bandwidth-total-graph': 'TrackBandwidthGraph--total-bandwidth-in-graph', + 'bandwidth-total-selection': 'TrackBandwidthGraph--total-bandwidth-in-range', + // Power - prefixes; the auto-scale ladder appends the unit suffix. + power: 'TrackPower--tooltip-power', + 'power-energy-preview': 'TrackPower--tooltip-energy-carbon-used-in-preview', + 'power-average-preview': 'TrackPower--tooltip-average-power', + 'power-energy-range': 'TrackPower--tooltip-energy-carbon-used-in-range', +}; + +let _co2: InstanceType | null = null; +function getCo2(): InstanceType { + if (_co2 === null) { + _co2 = new co2({ model: 'swd' }); + } + return _co2; +} + +// Bytes → CO₂e in grams. The 'device' grid intensity is zeroed so we don't +// double-count energy that the power track already attributes to the device. +export function carbonForBytes(bytes: number): number { + const co2eq = getCo2().perByteTrace(bytes, false, { + gridIntensity: { device: 0 }, + }); + return typeof co2eq.co2 === 'number' ? co2eq.co2 : co2eq.co2.total; +} + +// Watt-hours → CO₂e in grams. +export function carbonForWattHours( + wattHours: number, + meta: ProfileMeta +): number { + const intensity = meta.gramsOfCO2ePerKWh || averageIntensity.data.WORLD; + return (wattHours / 1000) * intensity; +} + +export type AutoScaleTier = { + // The first tier whose threshold the value meets (or, for the last tier, + // falls below) is chosen. + threshold: number; + // Applied to the input value to produce the displayed value. + multiplier: number; + // Applied to the carbon value (in grams) for the displayed unit. + carbonMultiplier: number; + // Appended to the row's Fluent ID prefix to pick the per-unit message. + suffix: string; + // Used when the row's label has no matching Fluent message. + unitText: string; + carbonUnitText: string; + valueSignificantDigits: number; + carbonSignificantDigits: number; +}; + +export const POWER_LADDER: AutoScaleTier[] = [ + { + threshold: 1000, + multiplier: 1 / 1000, + carbonMultiplier: 1 / 1000, + suffix: '-kilowatt', + unitText: 'kW', + carbonUnitText: 'kg CO₂e', + valueSignificantDigits: 3, + carbonSignificantDigits: 2, + }, + { + threshold: 1, + multiplier: 1, + carbonMultiplier: 1, + suffix: '-watt', + unitText: 'W', + carbonUnitText: 'g CO₂e', + valueSignificantDigits: 3, + carbonSignificantDigits: 3, + }, + { + threshold: 0.001, + multiplier: 1000, + carbonMultiplier: 1000, + suffix: '-milliwatt', + unitText: 'mW', + carbonUnitText: 'mg CO₂e', + valueSignificantDigits: 2, + carbonSignificantDigits: 2, + }, + { + threshold: -Infinity, + multiplier: 1000000, + carbonMultiplier: 1000, + suffix: '-microwatt', + unitText: 'µW', + carbonUnitText: 'mg CO₂e', + valueSignificantDigits: 2, + carbonSignificantDigits: 2, + }, +]; + +export const ENERGY_LADDER: AutoScaleTier[] = [ + { + threshold: 1000, + multiplier: 1 / 1000, + carbonMultiplier: 1 / 1000, + suffix: '-kilowatthour', + unitText: 'kWh', + carbonUnitText: 'kg CO₂e', + valueSignificantDigits: 3, + carbonSignificantDigits: 2, + }, + { + threshold: 1, + multiplier: 1, + carbonMultiplier: 1, + suffix: '-watthour', + unitText: 'Wh', + carbonUnitText: 'g CO₂e', + valueSignificantDigits: 3, + carbonSignificantDigits: 3, + }, + { + threshold: 0.001, + multiplier: 1000, + carbonMultiplier: 1000, + suffix: '-milliwatthour', + unitText: 'mWh', + carbonUnitText: 'mg CO₂e', + valueSignificantDigits: 2, + carbonSignificantDigits: 2, + }, + { + threshold: -Infinity, + multiplier: 1000000, + carbonMultiplier: 1000, + suffix: '-microwatthour', + unitText: 'µWh', + carbonUnitText: 'mg CO₂e', + valueSignificantDigits: 2, + carbonSignificantDigits: 2, + }, +]; + +export function pickTier( + value: number, + ladder: AutoScaleTier[] +): AutoScaleTier { + for (const tier of ladder) { + if (Math.abs(value) >= tier.threshold) { + return tier; + } + } + return ladder[ladder.length - 1]; +} diff --git a/src/components/tooltip/TrackPower.tsx b/src/components/tooltip/TrackPower.tsx deleted file mode 100644 index 984d98f564..0000000000 --- a/src/components/tooltip/TrackPower.tsx +++ /dev/null @@ -1,218 +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 { Localized } from '@fluent/react'; -import memoize from 'memoize-one'; - -import { averageIntensity } from '@tgwf/co2'; - -import explicitConnect from 'firefox-profiler/utils/connect'; -import { formatNumber } from 'firefox-profiler/utils/format-numbers'; -import { - getCommittedRange, - getPreviewSelection, - getProfileInterval, - getMeta, -} from 'firefox-profiler/selectors/profile'; -import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; -import { TooltipDetails, TooltipDetail } from './TooltipDetails'; - -import type { - State, - Counter, - Milliseconds, - PreviewSelection, - StartEndRange, - ProfileMeta, -} from 'firefox-profiler/types'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -type OwnProps = { - counter: Counter; - counterSampleIndex: number; -}; - -type StateProps = { - interval: Milliseconds; - meta: ProfileMeta; - committedRange: StartEndRange; - previewSelection: PreviewSelection | null; -}; - -type Props = ConnectedProps; - -class TooltipTrackPowerImpl extends React.PureComponent { - // This compute the sum of the power in the range. This returns a value in Wh. - _computePowerSumForRange(start: Milliseconds, end: Milliseconds): number { - const { counter } = this.props; - const samples = counter.samples; - const [beginIndex, endIndex] = getSampleIndexRangeForSelection( - samples, - start, - end - ); - - let sum = 0; - for ( - let counterSampleIndex = beginIndex; - counterSampleIndex < endIndex; - counterSampleIndex++ - ) { - sum += samples.count[counterSampleIndex]; // picowatt-hour; - } - return sum * 1e-12; - } - - _computeCO2eFromPower(power: number): number { - // total energy Wh to kWh - const energy = power / 1000; - const intensity = - this.props.meta.gramsOfCO2ePerKWh || averageIntensity.data.WORLD; - return energy * intensity; - } - - _computePowerSumForCommittedRange = memoize( - ({ start, end }: StartEndRange): number => - this._computePowerSumForRange(start, end) - ); - - _formatPowerValue( - power: number, - l10nIdKiloUnit: string, - l10nIdUnit: string, - l10nIdMilliUnit: string, - l10nIdMicroUnit: string - ): React.ReactElement { - let value, l10nId, carbonValue; - const carbon = this._computeCO2eFromPower(power); - if (power > 1000) { - value = formatNumber(power / 1000, 3); - carbonValue = formatNumber(carbon / 1000, 2); - l10nId = l10nIdKiloUnit; - } else if (power > 1) { - value = formatNumber(power, 3); - carbonValue = formatNumber(carbon, 3); - l10nId = l10nIdUnit; - } else if (power === 0) { - value = 0; - carbonValue = 0; - l10nId = l10nIdUnit; - } else if (power < 0.001) { - value = formatNumber(power * 1000000); - // Note: even though the power value is expressed in µWh, the carbon value - // is still expressed in mg. - carbonValue = formatNumber(carbon * 1000); - l10nId = l10nIdMicroUnit; - } else { - value = formatNumber(power * 1000); - carbonValue = formatNumber(carbon * 1000); - l10nId = l10nIdMilliUnit; - } - - return ( - - {value} - - ); - } - - maybeRenderForPreviewSelection( - previewSelection: PreviewSelection | null - ): React.ReactElement | null { - if (!previewSelection) { - return null; - } - - const { selectionStart, selectionEnd } = previewSelection; - const selectionRange = selectionEnd - selectionStart; - - if (selectionRange === 0) { - return null; - } - - const powerSumForPreviewRange = this._computePowerSumForRange( - selectionStart, - selectionEnd - ); - - return ( - <> - {this._formatPowerValue( - powerSumForPreviewRange, - 'TrackPower--tooltip-energy-carbon-used-in-preview-kilowatthour', - 'TrackPower--tooltip-energy-carbon-used-in-preview-watthour', - 'TrackPower--tooltip-energy-carbon-used-in-preview-milliwatthour', - 'TrackPower--tooltip-energy-carbon-used-in-preview-microwatthour' - )} - {this._formatPowerValue( - (1000 /* ms -> s */ * 3600 /* s -> h */ * powerSumForPreviewRange) / - selectionRange, - 'TrackPower--tooltip-average-power-kilowatt', - 'TrackPower--tooltip-average-power-watt', - 'TrackPower--tooltip-average-power-milliwatt', - 'TrackPower--tooltip-average-power-microwatt' - )} - - ); - } - - override render() { - const { - counter, - counterSampleIndex, - interval, - committedRange, - previewSelection, - } = this.props; - const samples = counter.samples; - - const powerUsageInPwh = samples.count[counterSampleIndex]; // picowatt-hour - const sampleTimeDeltaInMs = - counterSampleIndex === 0 - ? interval - : samples.time[counterSampleIndex] - - samples.time[counterSampleIndex - 1]; - const power = - ((powerUsageInPwh * 1e-12) /* pWh->Wh */ / sampleTimeDeltaInMs) * - 1000 * // ms->s - 3600; // s->h - - return ( -
- - {this._formatPowerValue( - power, - 'TrackPower--tooltip-power-kilowatt', - 'TrackPower--tooltip-power-watt', - 'TrackPower--tooltip-power-milliwatt', - 'TrackPower--tooltip-power-microwatt' - )} - {this.maybeRenderForPreviewSelection(previewSelection)} - {this._formatPowerValue( - this._computePowerSumForCommittedRange(committedRange), - 'TrackPower--tooltip-energy-carbon-used-in-range-kilowatthour', - 'TrackPower--tooltip-energy-carbon-used-in-range-watthour', - 'TrackPower--tooltip-energy-carbon-used-in-range-milliwatthour', - 'TrackPower--tooltip-energy-carbon-used-in-range-microwatthour' - )} - -
- ); - } -} - -export const TooltipTrackPower = explicitConnect({ - mapStateToProps: (state: State) => ({ - interval: getProfileInterval(state), - meta: getMeta(state), - committedRange: getCommittedRange(state), - previewSelection: getPreviewSelection(state), - }), - component: TooltipTrackPowerImpl, -}); diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index d1f44222dc..e040ac14fa 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -976,7 +976,7 @@ function _processSamples( /** * Derive a CounterDisplayConfig from a counter's category and name. */ -function _deriveCounterDisplay( +export function deriveCounterDisplay( category: string, name: string ): CounterDisplayConfig { @@ -988,6 +988,45 @@ function _deriveCounterDisplay( markerSchemaLocation: null, sortWeight: 10, label: 'Bandwidth', + tooltipRows: [ + { + type: 'value', + source: 'rate', + format: { unit: 'bytes-per-second', co2: 'per-byte' }, + label: 'Transfer speed for this sample', + labelKey: 'bandwidth-speed', + }, + { + type: 'value', + source: 'sample-number', + format: { unit: 'number' }, + label: 'read/write operations since the previous sample', + labelKey: 'bandwidth-operations', + }, + { type: 'separator' }, + { + type: 'value', + source: 'accumulated', + format: { unit: 'bytes', co2: 'per-byte' }, + label: 'Data transferred up to this time', + labelKey: 'bandwidth-cumulative', + }, + { + type: 'value', + source: 'count-range', + format: { unit: 'bytes', co2: 'per-byte' }, + label: 'Data transferred in the visible range', + labelKey: 'bandwidth-total-graph', + }, + { + type: 'value', + source: 'selection-total', + format: { unit: 'bytes', co2: 'per-byte' }, + label: 'Data transferred in the current selection', + labelKey: 'bandwidth-total-selection', + requiresPreviewSelection: true, + }, + ], }; } else if (category === 'Memory') { return { @@ -997,6 +1036,29 @@ function _deriveCounterDisplay( markerSchemaLocation: 'timeline-memory', sortWeight: 20, label: 'Memory', + tooltipRows: [ + { + type: 'value', + source: 'accumulated', + format: { unit: 'bytes' }, + label: 'relative memory at this time', + labelKey: 'memory-relative', + }, + { + type: 'value', + source: 'count-range', + format: { unit: 'bytes' }, + label: 'memory range in graph', + labelKey: 'memory-range', + }, + { + type: 'value', + source: 'sample-number', + format: { unit: 'number' }, + label: 'allocations and deallocations since the previous sample', + labelKey: 'memory-operations', + }, + ], }; } else if (category === 'power') { return { @@ -1006,6 +1068,38 @@ function _deriveCounterDisplay( markerSchemaLocation: null, sortWeight: 30, label: name, + tooltipRows: [ + { + type: 'value', + source: 'count', + format: { unit: 'number', co2: 'per-watthour', scale: 'power' }, + label: 'Power', + labelKey: 'power', + }, + { + type: 'value', + source: 'selection-total', + format: { unit: 'number', co2: 'per-watthour', scale: 'energy' }, + label: 'Energy used in the current selection', + labelKey: 'power-energy-preview', + requiresPreviewSelection: true, + }, + { + type: 'value', + source: 'selection-rate', + format: { unit: 'number', co2: 'per-watthour', scale: 'power' }, + label: 'Average power in the current selection', + labelKey: 'power-average-preview', + requiresPreviewSelection: true, + }, + { + type: 'value', + source: 'committed-range-total', + format: { unit: 'number', co2: 'per-watthour', scale: 'energy' }, + label: 'Energy used in the visible range', + labelKey: 'power-energy-range', + }, + ], }; } else if (category === 'CPU' && name === 'processCPU') { return { @@ -1015,6 +1109,15 @@ function _deriveCounterDisplay( markerSchemaLocation: null, sortWeight: 40, label: 'Process CPU', + tooltipRows: [ + { + type: 'value', + source: 'cpu-ratio', + format: { unit: 'percent' }, + label: 'CPU', + labelKey: 'cpu', + }, + ], }; } @@ -1025,6 +1128,14 @@ function _deriveCounterDisplay( markerSchemaLocation: null, sortWeight: 50, label: name, + tooltipRows: [ + { + type: 'value', + source: 'count', + format: { unit: 'number' }, + label: name, + }, + ], }; } @@ -1087,7 +1198,7 @@ function _processCounters( pid: mainThreadPid, mainThreadIndex, samples: adjustTableTimeDeltas(processedCounterSamples, delta), - display: _deriveCounterDisplay(category, name), + display: deriveCounterDisplay(category, name), }); return result; }, diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index 725b597997..c5e17208cf 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3109,6 +3109,137 @@ const _upgraders: { } } }, + [63]: (profile: any) => { + // Added tooltipRows to CounterDisplayConfig. This metadata describes the + // rows of the counter's hover tooltip (data source, value format, label). + // Derive defaults from the counter's category and name. + if (!profile.counters) { + return; + } + for (const counter of profile.counters) { + if (!counter.display || counter.display.tooltipRows !== undefined) { + continue; + } + const { category, name } = counter; + if (category === 'Bandwidth') { + counter.display.tooltipRows = [ + { + type: 'value', + source: 'rate', + format: { unit: 'bytes-per-second', co2: 'per-byte' }, + label: 'Transfer speed for this sample', + labelKey: 'bandwidth-speed', + }, + { + type: 'value', + source: 'sample-number', + format: { unit: 'number' }, + label: 'read/write operations since the previous sample', + labelKey: 'bandwidth-operations', + }, + { type: 'separator' }, + { + type: 'value', + source: 'accumulated', + format: { unit: 'bytes', co2: 'per-byte' }, + label: 'Data transferred up to this time', + labelKey: 'bandwidth-cumulative', + }, + { + type: 'value', + source: 'count-range', + format: { unit: 'bytes', co2: 'per-byte' }, + label: 'Data transferred in the visible range', + labelKey: 'bandwidth-total-graph', + }, + { + type: 'value', + source: 'selection-total', + format: { unit: 'bytes', co2: 'per-byte' }, + label: 'Data transferred in the current selection', + labelKey: 'bandwidth-total-selection', + requiresPreviewSelection: true, + }, + ]; + } else if (category === 'Memory') { + counter.display.tooltipRows = [ + { + type: 'value', + source: 'accumulated', + format: { unit: 'bytes' }, + label: 'relative memory at this time', + labelKey: 'memory-relative', + }, + { + type: 'value', + source: 'count-range', + format: { unit: 'bytes' }, + label: 'memory range in graph', + labelKey: 'memory-range', + }, + { + type: 'value', + source: 'sample-number', + format: { unit: 'number' }, + label: 'allocations and deallocations since the previous sample', + labelKey: 'memory-operations', + }, + ]; + } else if (category === 'power') { + counter.display.tooltipRows = [ + { + type: 'value', + source: 'count', + format: { unit: 'number', co2: 'per-watthour', scale: 'power' }, + label: 'Power', + labelKey: 'power', + }, + { + type: 'value', + source: 'selection-total', + format: { unit: 'number', co2: 'per-watthour', scale: 'energy' }, + label: 'Energy used in the current selection', + labelKey: 'power-energy-preview', + requiresPreviewSelection: true, + }, + { + type: 'value', + source: 'selection-rate', + format: { unit: 'number', co2: 'per-watthour', scale: 'power' }, + label: 'Average power in the current selection', + labelKey: 'power-average-preview', + requiresPreviewSelection: true, + }, + { + type: 'value', + source: 'committed-range-total', + format: { unit: 'number', co2: 'per-watthour', scale: 'energy' }, + label: 'Energy used in the visible range', + labelKey: 'power-energy-range', + }, + ]; + } else if (category === 'CPU' && name === 'processCPU') { + counter.display.tooltipRows = [ + { + type: 'value', + source: 'cpu-ratio', + format: { unit: 'percent' }, + label: 'CPU', + labelKey: 'cpu', + }, + ]; + } else { + counter.display.tooltipRows = [ + { + type: 'value', + source: 'count', + format: { unit: 'number' }, + label: name, + }, + ]; + } + } + }, // If you add a new upgrader here, please document the change in // `docs-developer/CHANGELOG-formats.md`. diff --git a/src/test/components/TrackMemory.test.tsx b/src/test/components/TrackMemory.test.tsx index 757293cd48..c0b10cff59 100644 --- a/src/test/components/TrackMemory.test.tsx +++ b/src/test/components/TrackMemory.test.tsx @@ -63,17 +63,10 @@ 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', - }; + const counter = getCounterForThread(thread, threadIndex, { + ...counterConfig, + category: 'Memory', + }); profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; diff --git a/src/test/components/TrackPower.test.tsx b/src/test/components/TrackPower.test.tsx index c952a33f35..ad5e936856 100644 --- a/src/test/components/TrackPower.test.tsx +++ b/src/test/components/TrackPower.test.tsx @@ -104,7 +104,7 @@ describe('TrackPower', function () { `Couldn't find the power canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackPowerTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getPowerDot = () => container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => diff --git a/src/test/components/TrackProcessCPU.test.tsx b/src/test/components/TrackProcessCPU.test.tsx index 9772159547..c7930deffa 100644 --- a/src/test/components/TrackProcessCPU.test.tsx +++ b/src/test/components/TrackProcessCPU.test.tsx @@ -184,6 +184,6 @@ describe('TrackProcessCPU', function () { // should be 50% CPU ratio since the highest value is 1000. But if we look // at the intervals, we can see that the interval of this sample is 0.5ms // therefore, the CPU usage should be 100% instead. - expect(screen.getByText(/CPU:/)).toHaveTextContent('100%'); + expect(screen.getByText(/CPU:/).nextSibling).toHaveTextContent('100%'); }); }); diff --git a/src/test/components/__snapshots__/TrackMemory.test.tsx.snap b/src/test/components/__snapshots__/TrackMemory.test.tsx.snap index 956d9e71d4..3b14e9eaa5 100644 --- a/src/test/components/__snapshots__/TrackMemory.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackMemory.test.tsx.snap @@ -12,34 +12,29 @@ exports[`TrackMemory has a tooltip that matches the snapshot 1`] = ` class="timelineTrackCounterTooltip" >
- - 0B - - relative memory at this time -
-
- + 0B +
- 2B - - memory range in graph -
-
- + 2B +
- 36 - - allocations and deallocations since the previous sample + allocations and deallocations since the previous sample + : +
+ 36
`; @@ -49,24 +44,22 @@ exports[`TrackMemory has a tooltip that matches the snapshot for counts equallin class="timelineTrackCounterTooltip" >
- - 0B - - relative memory at this time -
-
- + 0B +
- 2B - - memory range in graph + memory range in graph + : +
+ 2B
`; diff --git a/src/test/components/__snapshots__/TrackPower.test.tsx.snap b/src/test/components/__snapshots__/TrackPower.test.tsx.snap index bbc0ab47c7..08ff36d2f7 100644 --- a/src/test/components/__snapshots__/TrackPower.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackPower.test.tsx.snap @@ -9,7 +9,7 @@ exports[`TrackPower draws a dot that matches the snapshot 1`] = ` exports[`TrackPower has a tooltip that matches the snapshot 1`] = `
- CPU: - - - 50% - + CPU + : +
+ 50%
`; diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 428b559836..dad69f9e9b 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -11,6 +11,7 @@ import { } from '../../../profile-logic/data-structures'; import { mergeProfilesForDiffing } from '../../../profile-logic/merge-compare'; import { computeReferenceCPUDeltaPerMs } from '../../../profile-logic/cpu'; +import { deriveCounterDisplay } from '../../../profile-logic/process-profile'; import { stateFromLocation } from '../../../app-logic/url-handling'; import { StringTable } from '../../../utils/string-table'; import { computeThreadFromRawThread } from '../utils'; @@ -33,7 +34,6 @@ import type { CategoryList, JsTracerTable, RawCounter, - CounterDisplayConfig, TabID, MarkerPayload, NetworkPayload, @@ -1480,30 +1480,20 @@ export function getProfileWithJsTracerEvents( return profile; } -/** - * Default display configuration for test counters. - */ -const DEFAULT_TEST_COUNTER_DISPLAY: CounterDisplayConfig = { - graphType: 'line-rate', - unit: '', - color: 'grey', - markerSchemaLocation: null, - sortWeight: 50, - label: 'My Counter', -}; - /** * Creates a Counter fixture for a given thread. */ export function getCounterForThread( thread: RawThread, mainThreadIndex: ThreadIndex, - config: { hasCountNumber?: boolean } = {} + config: { hasCountNumber?: boolean; name?: string; category?: string } = {} ): RawCounter { const sampleTimes = computeTimeColumnForRawSamplesTable(thread.samples); + const name = config.name ?? 'My Counter'; + const category = config.category ?? 'My Category'; const counter: RawCounter = { - name: 'My Counter', - category: 'My Category', + name, + category, description: 'My Description', pid: thread.pid, mainThreadIndex, @@ -1517,7 +1507,7 @@ export function getCounterForThread( count: sampleTimes.map((_, i) => Math.sin(i)), length: thread.samples.length, }, - display: DEFAULT_TEST_COUNTER_DISPLAY, + display: deriveCounterDisplay(category, name), }; return counter; } @@ -1548,14 +1538,16 @@ export function getCounterForThreadWithSamples( length: samples.length, }; + const finalName = name ?? 'My Counter'; + const finalCategory = category ?? 'My Category'; const counter: RawCounter = { - name: name ?? 'My Counter', - category: category ?? 'My Category', + name: finalName, + category: finalCategory, description: 'My Description', pid: thread.pid, mainThreadIndex, samples: newSamples, - display: DEFAULT_TEST_COUNTER_DISPLAY, + display: deriveCounterDisplay(finalCategory, finalName), }; return counter; } diff --git a/src/test/fixtures/profiles/tracks.ts b/src/test/fixtures/profiles/tracks.ts index 88df189e41..f906fc5ad5 100644 --- a/src/test/fixtures/profiles/tracks.ts +++ b/src/test/fixtures/profiles/tracks.ts @@ -284,17 +284,9 @@ export function getStoreWithMemoryTrack(pid: Pid = '222') { thread.name = 'GeckoMain'; thread.isMainThread = true; 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', - }; + const counter = getCounterForThread(thread, threadIndex, { + category: 'Memory', + }); profile.counters = [counter]; } diff --git a/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap b/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap index 71147b11fc..49bb0217c5 100644 --- a/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap +++ b/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap @@ -87,7 +87,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -1386,7 +1386,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -2685,7 +2685,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -3984,7 +3984,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "a.out", "sampleUnits": Object { diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index 6702059ad9..c175dd3acc 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -423,7 +423,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "sourceURL": "", diff --git a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap index 6c0983e811..429f711cec 100644 --- a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap @@ -591,7 +591,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -79192,7 +79192,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -315562,7 +315562,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -350479,7 +350479,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -385375,7 +385375,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -387980,7 +387980,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -389809,7 +389809,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 355035987.653, @@ -393482,7 +393482,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -398697,7 +398697,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -399739,7 +399739,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -403614,7 +403614,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -418512,7 +418512,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -424719,7 +424719,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -441164,7 +441164,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -498819,7 +498819,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -561079,7 +561079,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "target/debug/examples/work_log (dhat)", "sourceURL": "", @@ -569632,7 +569632,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Flamegraph", "sourceURL": "", @@ -877518,7 +877518,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Flamegraph", "sourceURL": "", diff --git a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap index e85d4f0300..c42799e129 100644 --- a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap @@ -40,7 +40,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -7358,7 +7358,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -8693,7 +8693,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -9699,6 +9699,35 @@ Object { "label": "Memory", "markerSchemaLocation": "timeline-memory", "sortWeight": 20, + "tooltipRows": Array [ + Object { + "format": Object { + "unit": "bytes", + }, + "label": "relative memory at this time", + "labelKey": "memory-relative", + "source": "accumulated", + "type": "value", + }, + Object { + "format": Object { + "unit": "bytes", + }, + "label": "memory range in graph", + "labelKey": "memory-range", + "source": "count-range", + "type": "value", + }, + Object { + "format": Object { + "unit": "number", + }, + "label": "allocations and deallocations since the previous sample", + "labelKey": "memory-operations", + "source": "sample-number", + "type": "value", + }, + ], "unit": "bytes", }, "mainThreadIndex": 0, @@ -10163,7 +10192,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 62, + "preprocessedProfileVersion": 63, "processType": 0, "product": "Firefox", "stackwalk": 1, diff --git a/src/types/profile.ts b/src/types/profile.ts index 04d8a2ee79..4652e37c2a 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -536,6 +536,61 @@ export type GraphColor = export type CounterGraphType = 'line-accumulated' | 'line-rate'; +/** + * Per-sample data sources that a tooltip row can read from. + */ +export type CounterTooltipDataSource = + // samples.count[i] + | 'count' + // samples.count[i] / sampleTimeDelta[i] (per ms) + | 'rate' + // rate / maxCounterSampleCountPerMs (e.g., process CPU) + | 'cpu-ratio' + // accumulatedCounts[i] - minCount (cumulative sum minus baseline) + | 'accumulated' + // countRange across the visible (committed) graph + | 'count-range' + // Σ samples.count[i] over the preview selection + | 'selection-total' + // selection-total / selection-duration (per ms) + | 'selection-rate' + // Σ samples.count[i] over the committed range + | 'committed-range-total' + // samples.number[i] - the row is omitted when the column is absent. + | 'sample-number'; + +/** + * How a counter tooltip row's value should be formatted. + * - `unit`: the base formatter for the value. + * - `co2`: when set, an additional CO₂e estimate is shown next to the value. + * - `scale`: when set, the value is rendered using the named auto-scaling + * unit ladder (e.g., kW/W/mW/µW for `'power'`). + */ +export type CounterTooltipFormat = { + unit: 'bytes' | 'bytes-per-second' | 'percent' | 'number'; + co2?: 'per-byte' | 'per-watthour'; + scale?: 'power' | 'energy'; +}; + +/** + * One row inside a counter tooltip. + * + * - `label`: English text, used as the fallback when no translation applies. + * - `labelKey`: optional stable identifier the renderer maps to a translation. + * - `requiresPreviewSelection`: when true, hides the row unless there is a + * non-empty preview selection. + */ +export type CounterTooltipRow = + | { + type: 'value'; + source: CounterTooltipDataSource; + format: CounterTooltipFormat; + label: string; + labelKey?: string; + requiresPreviewSelection?: boolean; + } + | { type: 'separator' }; + /** * Specifies how a counter should be displayed in the UI. */ @@ -553,6 +608,8 @@ export type CounterDisplayConfig = { // types this is a friendly name (eg, "Memory"); for generic counters // it falls back to counter.name. label: string; + // Describes the rows shown in the hover tooltip. + tooltipRows: CounterTooltipRow[]; }; export type RawCounter = {