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 = {