From 21adfa0966bfb9b2036804c50c597a8646057288 Mon Sep 17 00:00:00 2001 From: fatadel Date: Mon, 30 Mar 2026 17:03:27 +0200 Subject: [PATCH 1/8] Add CounterDisplayConfig to counters in the processed profile format Make counters self-describing in terms of rendering by adding `display` field of `CounterDisplayConfig` type. The value is derived from a counter's `category` and `name` fields. This data is sufficient to understand how a counter should be rendered allowing us to remove hardcoded logic for each counter. This is the first PR for issue #5752. --- docs-developer/CHANGELOG-formats.md | 6 ++ src/app-logic/constants.ts | 2 +- src/profile-logic/process-profile.ts | 57 ++++++++++++++++++ .../processed-profile-versioning.ts | 60 +++++++++++++++++++ src/profile-logic/profile-data.ts | 2 +- .../fixtures/profiles/processed-profile.ts | 15 +++++ .../symbolicator-cli.test.ts.snap | 4 +- .../__snapshots__/profile-view.test.ts.snap | 2 +- .../profile-conversion.test.ts.snap | 36 +++++------ .../profile-upgrading.test.ts.snap | 16 +++-- src/types/profile-derived.ts | 2 + src/types/profile.ts | 22 +++++++ 12 files changed, 197 insertions(+), 27 deletions(-) diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index b1be78b6bb..50da5c96d4 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 62 + +A new `display` field of type `CounterDisplayConfig` was added to `RawCounter`. +This metadata makes counters self-describing in terms of how they are rendered in the UI. +For existing profiles, the display config is derived from the counter's `category` and `name`. + ### Version 61 The `SourceTable` in `profile.shared.sources` was updated: diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index f70f544144..14e3185680 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 = 61; +export const PROCESSED_PROFILE_VERSION = 62; // 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/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index fbf4c99cb5..d95355d18d 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -97,6 +97,7 @@ import type { GeckoSourceTable, IndexIntoCategoryList, IndexIntoFrameTable, + CounterDisplayConfig, } from 'firefox-profiler/types'; import { decompress, isGzip } from 'firefox-profiler/utils/gz'; @@ -972,6 +973,61 @@ function _processSamples( return samples; } +/** + * Derive a CounterDisplayConfig from a counter's category and name. + */ +function _deriveCounterDisplay( + category: string, + name: string +): CounterDisplayConfig { + if (category === 'Memory') { + return { + graphType: 'line-accumulated', + unit: 'bytes', + color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; + } else if (category === 'power') { + return { + graphType: 'line-rate', + unit: 'pWh', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 30, + label: name, + }; + } else if (category === 'Bandwidth') { + return { + graphType: 'line-rate', + unit: 'bytes', + color: 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'CPU' && name === 'processCPU') { + return { + graphType: 'line-rate', + unit: 'percent', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 40, + label: 'Process CPU', + }; + } + + return { + graphType: 'line-rate', + unit: '', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 50, + label: name, + }; +} + /** * Converts the Gecko list of counters into the processed format. */ @@ -1031,6 +1087,7 @@ function _processCounters( pid: mainThreadPid, mainThreadIndex, samples: adjustTableTimeDeltas(processedCounterSamples, delta), + 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 691380cf78..c6b48dec51 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3049,6 +3049,66 @@ const _upgraders: { } }, + [62]: (profile: any) => { + // Added CounterDisplayConfig to counters. This metadata controls how a + // counter is rendered (graph type, color, unit, etc.). + // Derive defaults from the counter's category and name. + if (profile.counters) { + for (const counter of profile.counters) { + if (counter.display !== undefined) { + continue; + } + const { category, name } = counter; + if (category === 'Memory') { + counter.display = { + graphType: 'line-accumulated', + unit: 'bytes', + color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; + } else if (category === 'power') { + counter.display = { + graphType: 'line-rate', + unit: 'pWh', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 30, + label: name, + }; + } else if (category === 'Bandwidth') { + counter.display = { + graphType: 'line-rate', + unit: 'bytes', + color: 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'CPU' && name === 'processCPU') { + counter.display = { + graphType: 'line-rate', + unit: 'percent', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 40, + label: 'Process CPU', + }; + } else { + counter.display = { + graphType: 'line-rate', + unit: '', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 50, + label: name, + }; + } + } + } + }, + // If you add a new upgrader here, please document the change in // `docs-developer/CHANGELOG-formats.md`. }; diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 645d8a9cc8..d4e7a5d964 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -2307,7 +2307,7 @@ export function processCounter(rawCounter: RawCounter): Counter { color: rawCounter.color, pid: rawCounter.pid, mainThreadIndex: rawCounter.mainThreadIndex, - + display: rawCounter.display, samples, }; diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 5961d2c0bf..428b559836 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -33,6 +33,7 @@ import type { CategoryList, JsTracerTable, RawCounter, + CounterDisplayConfig, TabID, MarkerPayload, NetworkPayload, @@ -1479,6 +1480,18 @@ 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. */ @@ -1504,6 +1517,7 @@ export function getCounterForThread( count: sampleTimes.map((_, i) => Math.sin(i)), length: thread.samples.length, }, + display: DEFAULT_TEST_COUNTER_DISPLAY, }; return counter; } @@ -1541,6 +1555,7 @@ export function getCounterForThreadWithSamples( pid: thread.pid, mainThreadIndex, samples: newSamples, + display: DEFAULT_TEST_COUNTER_DISPLAY, }; return counter; } diff --git a/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap b/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap index 19dd70789f..3af235db1a 100644 --- a/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap +++ b/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap @@ -87,7 +87,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -1415,7 +1415,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "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 f04baf9582..9f9343610e 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -418,7 +418,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "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 6a5e04368e..b804fe58c0 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": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -79192,7 +79192,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -315562,7 +315562,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -350479,7 +350479,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -385375,7 +385375,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -387980,7 +387980,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -389809,7 +389809,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 355035987.653, @@ -393482,7 +393482,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -398697,7 +398697,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -399739,7 +399739,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -403614,7 +403614,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -418512,7 +418512,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -424719,7 +424719,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -441164,7 +441164,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -498819,7 +498819,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -561079,7 +561079,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "target/debug/examples/work_log (dhat)", "sourceURL": "", @@ -563127,7 +563127,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Flamegraph", "sourceURL": "", @@ -871013,7 +871013,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "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 e139904c6d..e85d4f0300 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": 61, + "preprocessedProfileVersion": 62, "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": 61, + "preprocessedProfileVersion": 62, "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": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -9693,6 +9693,14 @@ Object { Object { "category": "Memory", "description": "Amount of allocated memory", + "display": Object { + "color": "orange", + "graphType": "line-accumulated", + "label": "Memory", + "markerSchemaLocation": "timeline-memory", + "sortWeight": 20, + "unit": "bytes", + }, "mainThreadIndex": 0, "name": "malloc", "pid": "11111", @@ -10155,7 +10163,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "stackwalk": 1, diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index f52454fd9e..e1a3802aa6 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -33,6 +33,7 @@ import type { IndexIntoFrameTable, SourceTable, IndexIntoSourceTable, + CounterDisplayConfig, } from './profile'; import type { IndexedArray } from './utils'; import type { BitSet } from '../utils/bitset'; @@ -191,6 +192,7 @@ export type Counter = { pid: Pid; mainThreadIndex: ThreadIndex; samples: CounterSamplesTable; + display: CounterDisplayConfig; }; /** diff --git a/src/types/profile.ts b/src/types/profile.ts index 90486b7ecd..70e2ba91d1 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -529,6 +529,27 @@ export type GraphColor = | 'teal' | 'yellow'; +export type CounterGraphType = 'line-accumulated' | 'line-rate'; + +/** + * Specifies how a counter should be displayed in the UI. + */ +export type CounterDisplayConfig = { + graphType: CounterGraphType; + unit: string; + color: GraphColor; + // The marker schema display location to filter markers for this track, + // e.g., "timeline-memory". If null, no markers are shown. + markerSchemaLocation: string | null; + // Controls the default display position of this counter track relative to + // other tracks. Tracks with lower values appear closer to the top. + sortWeight: number; + // The human-readable label shown in the track sidebar. For known counter + // types this is a friendly name (eg, "Memory"); for generic counters + // it falls back to counter.name. + label: string; +}; + export type RawCounter = { name: string; category: string; @@ -537,6 +558,7 @@ export type RawCounter = { pid: Pid; mainThreadIndex: ThreadIndex; samples: RawCounterSamplesTable; + display: CounterDisplayConfig; }; /** From 925b5f7a51dab114a90a275d2626c56582033a9a Mon Sep 17 00:00:00 2001 From: fatadel Date: Fri, 10 Apr 2026 14:04:37 +0200 Subject: [PATCH 2/8] Replace 4 counter track components with a single generic TrackCounter Collapse the separate Memory, Power, ProcessCPU, and Bandwidth track implementations into a single TrackCounter component that renders any counter type using CounterDisplayConfig from PR #5912. The LocalTrack union type is simplified from 8 variants to 5 by replacing 'memory', 'power', 'process-cpu', and 'bandwidth' with a single 'counter' type. The component branches on display.graphType for canvas drawing (accumulated vs rate) and on display.unit for tooltip rendering (bytes, pWh, percent, etc.). Track index ordering (for URL backward compatibility) is handled by a category-based mapping function. Display ordering uses the new display.sortWeight field. Part of #5752. --- src/actions/profile-view.ts | 5 +- src/app-logic/constants.ts | 25 +- src/components/timeline/LocalTrack.tsx | 20 +- src/components/timeline/TrackBandwidth.css | 25 - src/components/timeline/TrackBandwidth.tsx | 83 -- .../timeline/TrackBandwidthGraph.tsx | 712 -------------- .../{TrackMemory.css => TrackCounter.css} | 12 +- .../{TrackProcessCPU.tsx => TrackCounter.tsx} | 59 +- src/components/timeline/TrackCounterGraph.tsx | 877 ++++++++++++++++++ src/components/timeline/TrackMemory.tsx | 103 -- src/components/timeline/TrackMemoryGraph.tsx | 549 ----------- src/components/timeline/TrackPower.css | 25 - src/components/timeline/TrackPower.tsx | 83 -- src/components/timeline/TrackPowerGraph.tsx | 574 ------------ src/components/timeline/TrackProcessCPU.css | 39 - .../timeline/TrackProcessCPUGraph.tsx | 477 ---------- src/profile-logic/process-profile.ts | 4 +- .../processed-profile-versioning.ts | 4 +- src/profile-logic/tracks.ts | 170 ++-- src/selectors/app.tsx | 31 +- src/selectors/profile.ts | 13 +- src/test/components/TrackBandwidth.test.tsx | 53 +- src/test/components/TrackMemory.test.tsx | 29 +- src/test/components/TrackPower.test.tsx | 50 +- src/test/components/TrackProcessCPU.test.tsx | 46 +- .../__snapshots__/LocalTrack.test.tsx.snap | 6 +- .../TrackBandwidth.test.tsx.snap | 32 +- .../__snapshots__/TrackMemory.test.tsx.snap | 32 +- .../__snapshots__/TrackPower.test.tsx.snap | 10 +- .../TrackProcessCPU.test.tsx.snap | 42 +- .../fixtures/profiles/processed-profile.ts | 2 +- src/test/fixtures/profiles/tracks.ts | 32 +- src/test/store/profile-view.test.ts | 4 +- src/types/profile-derived.ts | 5 +- 34 files changed, 1252 insertions(+), 2981 deletions(-) delete mode 100644 src/components/timeline/TrackBandwidth.css delete mode 100644 src/components/timeline/TrackBandwidth.tsx delete mode 100644 src/components/timeline/TrackBandwidthGraph.tsx rename src/components/timeline/{TrackMemory.css => TrackCounter.css} (77%) rename src/components/timeline/{TrackProcessCPU.tsx => TrackCounter.tsx} (51%) create mode 100644 src/components/timeline/TrackCounterGraph.tsx delete mode 100644 src/components/timeline/TrackMemory.tsx delete mode 100644 src/components/timeline/TrackMemoryGraph.tsx delete mode 100644 src/components/timeline/TrackPower.css delete mode 100644 src/components/timeline/TrackPower.tsx delete mode 100644 src/components/timeline/TrackPowerGraph.tsx delete mode 100644 src/components/timeline/TrackProcessCPU.css delete mode 100644 src/components/timeline/TrackProcessCPUGraph.tsx diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index a7bf7ddc39..7950ec31a1 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -347,10 +347,7 @@ function getInformationFromTrackReference( relatedThreadIndex: localTrack.threadIndex, relatedTab: null, }; - case 'memory': - case 'bandwidth': - case 'process-cpu': - case 'power': { + case 'counter': { const counterSelectors = getCounterSelectors(localTrack.counterIndex); const counter = counterSelectors.getCounter(state); return { diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index 14e3185680..a8bdb0e23c 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -29,18 +29,10 @@ export const TRACK_NETWORK_ROW_REPEAT = 7; export const TRACK_NETWORK_HEIGHT = TRACK_NETWORK_ROW_HEIGHT * TRACK_NETWORK_ROW_REPEAT; -// The following values are for memory track. -export const TRACK_MEMORY_GRAPH_HEIGHT = 25; -export const TRACK_MEMORY_MARKERS_HEIGHT = 15; -export const TRACK_MEMORY_HEIGHT = - TRACK_MEMORY_GRAPH_HEIGHT + TRACK_MEMORY_MARKERS_HEIGHT; -export const TRACK_MEMORY_LINE_WIDTH = 2; -export const TRACK_MEMORY_DEFAULT_COLOR = 'orange'; - -// The following values are for the bandwidth track. -export const TRACK_BANDWIDTH_HEIGHT = 25; -export const TRACK_BANDWIDTH_LINE_WIDTH = 2; -export const TRACK_BANDWIDTH_DEFAULT_COLOR = 'blue'; +// The following values are for counter tracks (Memory, Power, Bandwidth, etc.). +export const TRACK_COUNTER_GRAPH_HEIGHT = 25; +export const TRACK_COUNTER_MARKERS_HEIGHT = 15; +export const TRACK_COUNTER_LINE_WIDTH = 2; // The following values are for experimental event delay track. export const TRACK_EVENT_DELAY_HEIGHT = 40; @@ -61,15 +53,6 @@ export const TRACK_PROCESS_BLANK_HEIGHT = 30; // Height of timeline ruler. export const TIMELINE_RULER_HEIGHT = 20; -// Height of the power track. -export const TRACK_POWER_HEIGHT = 25; -export const TRACK_POWER_LINE_WIDTH = 2; -export const TRACK_POWER_DEFAULT_COLOR = 'grey'; - -// Height of the process cpu track. -export const TRACK_PROCESS_CPU_HEIGHT = 25; -export const TRACK_PROCESS_CPU_LINE_WIDTH = 2; - // JS Tracer has very high fidelity information, and needs a more fine-grained zoom. export const JS_TRACER_MAXIMUM_CHART_ZOOM = 0.001; diff --git a/src/components/timeline/LocalTrack.tsx b/src/components/timeline/LocalTrack.tsx index f43e2b991f..ff670bb722 100644 --- a/src/components/timeline/LocalTrack.tsx +++ b/src/components/timeline/LocalTrack.tsx @@ -26,11 +26,8 @@ import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { TimelineTrackThread } from './TrackThread'; import { TrackEventDelay } from './TrackEventDelay'; import { TrackNetwork } from './TrackNetwork'; -import { TrackMemory } from './TrackMemory'; -import { TrackBandwidth } from './TrackBandwidth'; +import { TrackCounter } from './TrackCounter'; import { TrackIPC } from './TrackIPC'; -import { TrackProcessCPU } from './TrackProcessCPU'; -import { TrackPower } from './TrackPower'; import { getTrackSelectionModifiers } from 'firefox-profiler/utils'; import type { TrackReference, @@ -106,18 +103,12 @@ class LocalTrackComponent extends PureComponent { ); case 'network': return ; - case 'memory': - return ; - case 'bandwidth': - return ; + case 'counter': + return ; case 'ipc': return ; case 'event-delay': return ; - case 'process-cpu': - return ; - case 'power': - return ; case 'marker': return ( ; - -type State = {}; - -export class TrackBandwidthImpl extends React.PureComponent { - override render() { - const { counterIndex } = this.props; - return ( -
- -
- ); - } -} - -export const TrackBandwidth = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - return { - threadIndex: counter.mainThreadIndex, - rangeStart: start, - rangeEnd: end, - }; - }, - component: TrackBandwidthImpl, -}); diff --git a/src/components/timeline/TrackBandwidthGraph.tsx b/src/components/timeline/TrackBandwidthGraph.tsx deleted file mode 100644 index 6f05c80bd2..0000000000 --- a/src/components/timeline/TrackBandwidthGraph.tsx +++ /dev/null @@ -1,712 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { InView } from 'react-intersection-observer'; -import { Localized } from '@fluent/react'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { - getStrokeColor, - getFillColor, - getDotColor, -} from 'firefox-profiler/profile-logic/graph-color'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - formatBytes, - formatNumber, -} from 'firefox-profiler/utils/format-numbers'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getPreviewSelection, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { - TooltipDetails, - TooltipDetail, - TooltipDetailSeparator, -} from 'firefox-profiler/components/tooltip/TooltipDetails'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { TRACK_BANDWIDTH_DEFAULT_COLOR } from 'firefox-profiler/app-logic/constants'; -import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; -import { co2 } from '@tgwf/co2'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - AccumulatedCounterSamples, - Milliseconds, - PreviewSelection, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackBandwidth.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackBandwidthCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _requestedAnimationFrame: boolean = false; - _canvasState: { renderScheduled: boolean; inView: boolean } = { - renderScheduled: false, - inView: false, - }; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - maxCounterSampleCountPerMs, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - // Take the sample information, and convert it into chart coordinates. Use a slightly - // smaller space than the deviceHeight, so that the stroke will be fully visible - // both at the top and bottom of the chart. - const [sampleStart, sampleEnd] = counterSampleRange; - const countRangePerMs = maxCounterSampleCountPerMs; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // bandwidth graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor( - counter.color || TRACK_BANDWIDTH_DEFAULT_COLOR - ); - ctx.fillStyle = getFillColor( - counter.color || TRACK_BANDWIDTH_DEFAULT_COLOR - ); - ctx.beginPath(); - - const getX = (i: number) => - Math.round((samples.time[i] - rangeStart) * millisecondWidth); - const getY = (i: number) => { - const rawY = samples.count[i]; - if (!rawY) { - // Make the 0 values invisible so that 'almost 0' is noticeable. - return deviceHeight + deviceLineHalfWidth; - } - - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - const unitGraphCount = rawY / sampleTimeDeltaInMs / countRangePerMs; - return ( - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - deviceLineHalfWidth - ); - }; - - // The x and y are used after the loop. - const firstX = getX(sampleStart); - let x = firstX; - let y = getY(sampleStart); - - // For the first sample, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - ctx.moveTo(x, y); - - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - for (let i = sampleStart + 1; i < sampleEnd; i++) { - x = getX(i); - y = getY(i); - ctx.lineTo(x, y); - - // If we have multiple samples to draw on the same horizontal pixel, - // we process all of them together with a max-min decimation algorithm - // to save time: - // - We draw the first and last samples to ensure the display is - // correct if there are sampling gaps. - // - For the values in between, we only draw the min and max values, - // to draw a vertical line covering all the other sample values. - const values = [y]; - while (i + 1 < sampleEnd && getX(i + 1) === x) { - values.push(getY(++i)); - } - - // Looking for the min and max only makes sense if we have more than 2 - // samples to draw. - if (values.length > 2) { - const maxY = Math.max(...values); - if (maxY !== y) { - y = maxY; - ctx.lineTo(x, y); - } - const minY = Math.min(...values); - if (minY !== y) { - y = minY; - ctx.lineTo(x, y); - } - } - - const lastY = values[values.length - 1]; - if (lastY !== y) { - y = lastY; - ctx.lineTo(x, y); - } - } - - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _scheduleDraw() { - if (!this._canvasState.inView) { - // Canvas is not in the view. Schedule the render for a later intersection - // observer callback. - this._canvasState.renderScheduled = true; - return; - } - - // Canvas is in the view. Render the canvas and reset the schedule state. - this._canvasState.renderScheduled = false; - - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - const canvas = this._canvas; - if (canvas) { - this.drawCanvas(canvas); - } - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { - this._canvasState.inView = inView; - if (!this._canvasState.renderScheduled) { - // Skip if render is not scheduled. - return; - } - - this._scheduleDraw(); - }; - - override componentDidMount() { - this._scheduleDraw(); - } - - override componentDidUpdate() { - this._scheduleDraw(); - } - - override render() { - return ( - - - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; - readonly previewSelection: PreviewSelection | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The bandwidth track graph takes bandwidth information from counters, and renders it as a - * graph in the timeline. - */ -class TrackBandwidthGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - // This persistTooltips property is part of the web console API. It helps - // in being able to inspect and debug tooltips. - if (window.persistTooltips) { - return; - } - - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - if (counter.samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample group found for bandwidth counter'); - } - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - - // If there are samples before or after hoveredCounter that fall - // horizontally on the same pixel, move hoveredCounter to the sample - // with the highest power value. - const mouseAtTime = (t: number) => - Math.round(((t - rangeStart) / rangeLength) * width + left); - for ( - let currentIndex = hoveredCounter - 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex > 0; - --currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - for ( - let currentIndex = hoveredCounter + 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex < samples.time.length; - ++currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _co2: InstanceType | null = null; - _formatDataTransferValue(bytes: number, l10nId: string) { - if (!this._co2) { - this._co2 = new co2({ model: 'swd' }); - } - // By default when estimating emissions per byte, co2.js takes into account - // emissions for the user device, the data center and the network. - // Because we already have power tracks showing the power use and estimated - // emissions of the device, set the 'device' grid intensity to 0 to avoid - // double counting. - const co2eq = this._co2!.perByteTrace(bytes, false, { - gridIntensity: { device: 0 }, - }); - const carbonValue = formatNumber( - typeof co2eq.co2 === 'number' ? co2eq.co2 : co2eq.co2.total - ); - const value = formatBytes(bytes); - return ( - - {value} - - ); - } - - _renderTooltip(counterIndex: number): React.ReactNode { - const { - accumulatedSamples, - counter, - rangeStart, - rangeEnd, - interval, - previewSelection, - } = this.props; - const { mouseX, mouseY } = this.state; - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No accumulated sample found for bandwidth counter'); - } - - const sampleTime = samples.time[counterIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const bytes = accumulatedCounts[counterIndex] - minCount; - const operations = - samples.number !== undefined ? samples.number[counterIndex] : null; - - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitGraphCount = samples.count[counterIndex] / sampleTimeDeltaInMs; - - let rangeTotal = 0; - if (previewSelection) { - const [beginIndex, endIndex] = getSampleIndexRangeForSelection( - samples, - previewSelection.selectionStart, - previewSelection.selectionEnd - ); - - for ( - let counterSampleIndex = beginIndex; - counterSampleIndex < endIndex; - counterSampleIndex++ - ) { - rangeTotal += samples.count[counterSampleIndex]; - } - } - - let ops; - if (operations !== null) { - ops = formatNumber(operations, 2, 0); - } - - return ( - -
- - {this._formatDataTransferValue( - unitGraphCount * 1000 /* ms -> s */, - 'TrackBandwidthGraph--speed' - )} - {operations !== null ? ( - - {ops} - - ) : null} - - {this._formatDataTransferValue( - bytes, - 'TrackBandwidthGraph--cumulative-bandwidth-at-this-time' - )} - {this._formatDataTransferValue( - countRange, - 'TrackBandwidthGraph--total-bandwidth-in-graph' - )} - {previewSelection - ? this._formatDataTransferValue( - rangeTotal, - 'TrackBandwidthGraph--total-bandwidth-in-range' - ) - : null} - -
-
- ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderBandwidthDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - interval, - } = this.props; - - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for bandwidth counter'); - } - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = (width * (sampleTime - rangeStart)) / rangeLength; - const countRangePerMs = maxCounterSampleCountPerMs; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / sampleTimeDeltaInMs / countRangePerMs; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - accumulatedSamples, - maxCounterSampleCountPerMs, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderBandwidthDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackBandwidthGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - maxCounterSampleCountPerMs: - counterSelectors.getMaxRangeCounterSampleCountPerMs(state), - accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - previewSelection: getPreviewSelection(state), - }; - }, - component: withSize(TrackBandwidthGraphImpl), -}); diff --git a/src/components/timeline/TrackMemory.css b/src/components/timeline/TrackCounter.css similarity index 77% rename from src/components/timeline/TrackMemory.css rename to src/components/timeline/TrackCounter.css index 404b625b0c..c45e73fdf9 100644 --- a/src/components/timeline/TrackMemory.css +++ b/src/components/timeline/TrackCounter.css @@ -2,19 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.timelineTrackMemoryGraph { +.timelineTrackCounterGraph { position: relative; width: 100%; height: var(--graph-height); } -.timelineTrackMemoryCanvas { +.timelineTrackCounterCanvas { position: absolute; width: 100%; height: 100%; } -.timelineTrackMemoryGraphDot { +.timelineTrackCounterGraphDot { position: absolute; width: 6px; height: 6px; @@ -24,18 +24,18 @@ pointer-events: none; } -.timelineTrackMemoryTooltipLine { +.timelineTrackCounterTooltipLine { white-space: nowrap; } -.timelineTrackMemoryTooltipNumber { +.timelineTrackCounterTooltipNumber { display: inline-block; min-width: 60px; color: var(--tooltip-number-foreground-color); font-weight: bold; } -.timelineMarkersMemory { +.timelineTrackCounterMarkers { height: var(--markers-height, 15px); opacity: 1; } diff --git a/src/components/timeline/TrackProcessCPU.tsx b/src/components/timeline/TrackCounter.tsx similarity index 51% rename from src/components/timeline/TrackProcessCPU.tsx rename to src/components/timeline/TrackCounter.tsx index 44b6eb15b6..dfc7c005b2 100644 --- a/src/components/timeline/TrackProcessCPU.tsx +++ b/src/components/timeline/TrackCounter.tsx @@ -8,22 +8,25 @@ import { getCommittedRange, getCounterSelectors, } from 'firefox-profiler/selectors/profile'; +import { TimelineMarkersMemory } from './Markers'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackProcessCPUGraph } from './TrackProcessCPUGraph'; +import { TrackCounterGraph } from './TrackCounterGraph'; import { - TRACK_PROCESS_CPU_HEIGHT, - TRACK_PROCESS_CPU_LINE_WIDTH, + TRACK_COUNTER_GRAPH_HEIGHT, + TRACK_COUNTER_MARKERS_HEIGHT, + TRACK_COUNTER_LINE_WIDTH, } from 'firefox-profiler/app-logic/constants'; import type { CounterIndex, ThreadIndex, Milliseconds, + CounterDisplayConfig, } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; -import './TrackProcessCPU.css'; +import './TrackCounter.css'; type OwnProps = { readonly counterIndex: CounterIndex; @@ -33,6 +36,7 @@ type StateProps = { readonly threadIndex: ThreadIndex; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; + readonly display: CounterDisplayConfig; }; type DispatchProps = { @@ -41,32 +45,56 @@ type DispatchProps = { type Props = ConnectedProps; -type State = {}; +export class TrackCounterImpl extends React.PureComponent { + _onMarkerSelect = (start: Milliseconds, end: Milliseconds) => { + const { rangeStart, rangeEnd, updatePreviewSelection } = this.props; + updatePreviewSelection({ + isModifying: false, + selectionStart: Math.max(rangeStart, start), + selectionEnd: Math.min(rangeEnd, end), + }); + }; -export class TrackProcessCPUImpl extends React.PureComponent { override render() { - const { counterIndex } = this.props; + const { counterIndex, rangeStart, rangeEnd, threadIndex, display } = + this.props; + + const hasMarkers = display.markerSchemaLocation !== null; + const graphHeight = TRACK_COUNTER_GRAPH_HEIGHT; + const totalHeight = hasMarkers + ? graphHeight + TRACK_COUNTER_MARKERS_HEIGHT + : graphHeight; + return (
- + ) : null} +
); } } -export const TrackProcessCPU = explicitConnect< +export const TrackCounter = explicitConnect< OwnProps, StateProps, DispatchProps @@ -80,8 +108,9 @@ export const TrackProcessCPU = explicitConnect< threadIndex: counter.mainThreadIndex, rangeStart: start, rangeEnd: end, + display: counter.display, }; }, mapDispatchToProps: { updatePreviewSelection }, - component: TrackProcessCPUImpl, + component: TrackCounterImpl, }); diff --git a/src/components/timeline/TrackCounterGraph.tsx b/src/components/timeline/TrackCounterGraph.tsx new file mode 100644 index 0000000000..0f70a5c7c8 --- /dev/null +++ b/src/components/timeline/TrackCounterGraph.tsx @@ -0,0 +1,877 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; +import { InView } from 'react-intersection-observer'; +import { Localized } from '@fluent/react'; +import { withSize } from 'firefox-profiler/components/shared/WithSize'; +import { + getStrokeColor, + getFillColor, + getDotColor, +} from 'firefox-profiler/profile-logic/graph-color'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { + formatBytes, + formatNumber, + formatPercent, +} from 'firefox-profiler/utils/format-numbers'; +import { bisectionRight } from 'firefox-profiler/utils/bisect'; +import { + getCommittedRange, + getCounterSelectors, + getPreviewSelection, + getProfileInterval, +} from 'firefox-profiler/selectors/profile'; +import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; +import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; +import { TooltipTrackPower } from 'firefox-profiler/components/tooltip/TrackPower'; +import { + TooltipDetails, + TooltipDetail, + TooltipDetailSeparator, +} from 'firefox-profiler/components/tooltip/TooltipDetails'; +import { EmptyThreadIndicator } from './EmptyThreadIndicator'; +import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; +import { co2 } from '@tgwf/co2'; + +import type { + CounterIndex, + Counter, + Thread, + ThreadIndex, + AccumulatedCounterSamples, + Milliseconds, + PreviewSelection, + CssPixels, + StartEndRange, + IndexIntoSamplesTable, + CounterDisplayConfig, +} from 'firefox-profiler/types'; + +import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './TrackCounter.css'; + +/** + * When adding properties to these props, please consider the comment above `TrackCounterCanvas`. + */ +type CanvasProps = { + readonly rangeStart: Milliseconds; + readonly rangeEnd: Milliseconds; + readonly counter: Counter; + readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; + readonly accumulatedSamples: AccumulatedCounterSamples; + readonly maxCounterSampleCountPerMs: number; + readonly interval: Milliseconds; + readonly width: CssPixels; + readonly height: CssPixels; + readonly lineWidth: CssPixels; + readonly display: CounterDisplayConfig; +}; + +/** + * This component controls the rendering of the canvas. Every render call through + * React triggers a new canvas render. Because of this, it's important to only pass + * in the props that are needed for the canvas draw call. + */ +class TrackCounterCanvas extends React.PureComponent { + _canvas: null | HTMLCanvasElement = null; + _requestedAnimationFrame: boolean = false; + _canvasState: { renderScheduled: boolean; inView: boolean } = { + renderScheduled: false, + inView: false, + }; + + drawCanvas(canvas: HTMLCanvasElement): void { + const { + rangeStart, + rangeEnd, + counter, + height, + width, + lineWidth, + interval, + accumulatedSamples, + maxCounterSampleCountPerMs, + counterSampleRange, + display, + } = this.props; + if (width === 0) { + // Attempt to draw before the canvas was laid out. + return; + } + + const ctx = canvas.getContext('2d')!; + const devicePixelRatio = window.devicePixelRatio; + const deviceWidth = width * devicePixelRatio; + const deviceHeight = height * devicePixelRatio; + const deviceLineWidth = lineWidth * devicePixelRatio; + const deviceLineHalfWidth = deviceLineWidth * 0.5; + const innerDeviceHeight = deviceHeight - deviceLineWidth; + const rangeLength = rangeEnd - rangeStart; + const millisecondWidth = deviceWidth / rangeLength; + const intervalWidth = interval * millisecondWidth; + + // Resize and clear the canvas. + canvas.width = Math.round(deviceWidth); + canvas.height = Math.round(deviceHeight); + ctx.clearRect(0, 0, deviceWidth, deviceHeight); + + const samples = counter.samples; + if (samples.length === 0) { + // There's no reason to draw the samples, there are none. + return; + } + + // Take the sample information, and convert it into chart coordinates. Use a slightly + // smaller space than the deviceHeight, so that the stroke will be fully visible + // both at the top and bottom of the chart. + const [sampleStart, sampleEnd] = counterSampleRange; + + { + // Draw the chart. + // + // ...--` + // 1 ...---```..-- `--. 2 + // |_____________________| + // 4 3 + // + // Start by drawing from 1 to 2. This will be the top of all the peaks of the + // counter graph. + + ctx.lineWidth = deviceLineWidth; + ctx.lineJoin = 'bevel'; + ctx.strokeStyle = getStrokeColor(display.color); + ctx.fillStyle = getFillColor(display.color); + ctx.beginPath(); + + if (display.graphType === 'line-accumulated') { + // Accumulated graph: plot the running total. + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + + // The x and y are used after the loop. + let x = 0; + let y = 0; + let firstX = 0; + for (let i = sampleStart; i < sampleEnd; i++) { + // Create a path for the top of the chart. This is the line that will have + // a stroke applied to it. + x = (samples.time[i] - rangeStart) * millisecondWidth; + // Add on half the stroke's line width so that it won't be cut off the edge + // of the graph. + const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange; + y = + innerDeviceHeight - + innerDeviceHeight * unitGraphCount + + deviceLineHalfWidth; + if (i === sampleStart) { + // This is the first iteration, only move the line, do not draw it. Also + // remember this first X, as the bottom of the graph will need to connect + // back up to it. + firstX = x; + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + // The samples range ends at the time of the last sample, plus the interval. + // Draw this last bit. + ctx.lineTo(x + intervalWidth, y); + + // Don't do the fill yet, just stroke the top line. This will draw a line from + // point 1 to 2 in the diagram above. + ctx.stroke(); + + // After doing the stroke, continue the path to complete the fill to the bottom + // of the canvas. This continues the path to point 3 and then 4. + + // Create a line from 2 to 3. + ctx.lineTo(x + intervalWidth, deviceHeight); + + // Create a line from 3 to 4. + ctx.lineTo(firstX, deviceHeight); + + // The line from 4 to 1 will be implicitly filled in. + ctx.fill(); + } else { + // Rate graph: plot count / timeDelta with min-max decimation. + const countRangePerMs = maxCounterSampleCountPerMs; + + const getX = (i: number) => + Math.round((samples.time[i] - rangeStart) * millisecondWidth); + const getY = (rawY: number) => { + if (!rawY) { + // Make the 0 values invisible so that 'almost 0' is noticeable. + return deviceHeight + deviceLineHalfWidth; + } + const unitGraphCount = rawY / countRangePerMs; + return Math.round( + innerDeviceHeight - + innerDeviceHeight * unitGraphCount + + // Add on half the stroke's line width so that it won't be cut off the edge + // of the graph. + deviceLineHalfWidth + ); + }; + + const getRate = (i: number) => { + const sampleTimeDeltaInMs = + i === 0 ? interval : samples.time[i] - samples.time[i - 1]; + return samples.count[i] / sampleTimeDeltaInMs; + }; + + // The x and y are used after the loop. + const firstX = getX(sampleStart); + let x = firstX; + let y = getY(getRate(sampleStart)); + + // For the first sample, only move the line, do not draw it. Also + // remember this first X, as the bottom of the graph will need to connect + // back up to it. + ctx.moveTo(x, y); + + // Create a path for the top of the chart. This is the line that will have + // a stroke applied to it. + for (let i = sampleStart + 1; i < sampleEnd; i++) { + const rateValues = [getRate(i)]; + x = getX(i); + y = getY(rateValues[0]); + ctx.lineTo(x, y); + + // If we have multiple samples to draw on the same horizontal pixel, + // we process all of them together with a max-min decimation algorithm + // to save time: + // - We draw the first and last samples to ensure the display is + // correct if there are sampling gaps. + // - For the values in between, we only draw the min and max values, + // to draw a vertical line covering all the other sample values. + while (i + 1 < sampleEnd && getX(i + 1) === x) { + rateValues.push(getRate(++i)); + } + + // Looking for the min and max only makes sense if we have more than 2 + // samples to draw. + if (rateValues.length > 2) { + const minY = getY(Math.min(...rateValues)); + if (minY !== y) { + y = minY; + ctx.lineTo(x, y); + } + const maxY = getY(Math.max(...rateValues)); + if (maxY !== y) { + y = maxY; + ctx.lineTo(x, y); + } + } + + const lastY = getY(rateValues[rateValues.length - 1]); + if (lastY !== y) { + y = lastY; + ctx.lineTo(x, y); + } + } + + // The samples range ends at the time of the last sample, plus the interval. + // Draw this last bit. + ctx.lineTo(x + intervalWidth, y); + + // Don't do the fill yet, just stroke the top line. This will draw a line from + // point 1 to 2 in the diagram above. + ctx.stroke(); + + // After doing the stroke, continue the path to complete the fill to the bottom + // of the canvas. This continues the path to point 3 and then 4. + + // Create a line from 2 to 3. + ctx.lineTo(x + intervalWidth, deviceHeight); + + // Create a line from 3 to 4. + ctx.lineTo(firstX, deviceHeight); + + // The line from 4 to 1 will be implicitly filled in. + ctx.fill(); + } + } + } + + _scheduleDraw() { + if (!this._canvasState.inView) { + // Canvas is not in the view. Schedule the render for a later intersection + // observer callback. + this._canvasState.renderScheduled = true; + return; + } + + // Canvas is in the view. Render the canvas and reset the schedule state. + this._canvasState.renderScheduled = false; + + if (!this._requestedAnimationFrame) { + this._requestedAnimationFrame = true; + window.requestAnimationFrame(() => { + this._requestedAnimationFrame = false; + const canvas = this._canvas; + if (canvas) { + this.drawCanvas(canvas); + } + }); + } + } + + _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { + this._canvas = canvas; + }; + + _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { + this._canvasState.inView = inView; + if (!this._canvasState.renderScheduled) { + // Skip if render is not scheduled. + return; + } + + this._scheduleDraw(); + }; + + override componentDidMount() { + this._scheduleDraw(); + } + + override componentDidUpdate() { + this._scheduleDraw(); + } + + override render() { + return ( + + + + ); + } +} + +type OwnProps = { + readonly counterIndex: CounterIndex; + readonly lineWidth: CssPixels; + readonly graphHeight: CssPixels; +}; + +type StateProps = { + readonly threadIndex: ThreadIndex; + readonly rangeStart: Milliseconds; + readonly rangeEnd: Milliseconds; + readonly counter: Counter; + readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; + readonly accumulatedSamples: AccumulatedCounterSamples; + readonly maxCounterSampleCountPerMs: number; + readonly interval: Milliseconds; + readonly filteredThread: Thread; + readonly unfilteredSamplesRange: StartEndRange | null; + readonly previewSelection: PreviewSelection | null; + readonly display: CounterDisplayConfig; +}; + +type DispatchProps = {}; + +type Props = SizeProps & ConnectedProps; + +type State = { + hoveredCounter: null | number; + mouseX: CssPixels; + mouseY: CssPixels; +}; + +/** + * The generic counter track graph component. It renders information from any counters + * (eg, Memory, Power, etc.) as a graph in the timeline. It handles all counter types by + * branching on `display.graphType` for drawing and `display.unit` for tooltips. + */ +class TrackCounterGraphImpl extends React.PureComponent { + override state = { + hoveredCounter: null, + mouseX: 0, + mouseY: 0, + }; + + _co2: InstanceType | null = null; + + _onMouseLeave = () => { + // This persistTooltips property is part of the web console API. It helps + // in being able to inspect and debug tooltips. + if (window.persistTooltips) { + return; + } + + this.setState({ hoveredCounter: null }); + }; + + _onMouseMove = (event: React.MouseEvent) => { + const { pageX: mouseX, pageY: mouseY } = event; + // Get the offset from here, and apply it to the time lookup. + const { left } = event.currentTarget.getBoundingClientRect(); + const { + width, + rangeStart, + rangeEnd, + counter, + interval, + counterSampleRange, + } = this.props; + const rangeLength = rangeEnd - rangeStart; + const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; + + if (counter.samples.length === 0) { + throw new Error('No sample group found for counter'); + } + const { samples } = counter; + + if ( + timeAtMouse < samples.time[0] || + timeAtMouse > samples.time[samples.length - 1] + interval + ) { + // We are outside the range of the samples, do not display hover information. + this.setState({ hoveredCounter: null }); + } else { + // When the mouse pointer hovers between two points, select the point that's closer. + let hoveredCounter; + const [sampleStart, sampleEnd] = counterSampleRange; + const bisectionCounter = bisectionRight( + samples.time, + timeAtMouse, + sampleStart, + sampleEnd + ); + if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { + const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; + const rightDistance = samples.time[bisectionCounter] - timeAtMouse; + if (leftDistance < rightDistance) { + // Left point is closer + hoveredCounter = bisectionCounter - 1; + } else { + // Right point is closer + hoveredCounter = bisectionCounter; + } + + // For rate-based graphs with decimation, find the sample with the + // highest value at the same pixel position. + if (this.props.display.graphType === 'line-rate') { + const mouseAtTime = (t: number) => + Math.round(((t - rangeStart) / rangeLength) * width + left); + for ( + let currentIndex = hoveredCounter - 1; + mouseAtTime(samples.time[currentIndex]) === mouseX && + currentIndex > 0; + --currentIndex + ) { + if (samples.count[currentIndex] > samples.count[hoveredCounter]) { + hoveredCounter = currentIndex; + } + } + for ( + let currentIndex = hoveredCounter + 1; + mouseAtTime(samples.time[currentIndex]) === mouseX && + currentIndex < samples.time.length; + ++currentIndex + ) { + if (samples.count[currentIndex] > samples.count[hoveredCounter]) { + hoveredCounter = currentIndex; + } + } + } + } else { + hoveredCounter = bisectionCounter; + } + + if (hoveredCounter === samples.length) { + // When hovering the last sample, it's possible the mouse is past the time. + // In this case, hover over the last sample. This happens because of the + // ` + interval` line in the `if` condition above. + hoveredCounter = samples.time.length - 1; + } + + this.setState({ + mouseX, + mouseY, + hoveredCounter, + }); + } + }; + + _formatDataTransferValue(bytes: number, l10nId: string) { + if (!this._co2) { + this._co2 = new co2({ model: 'swd' }); + } + // By default, when estimating emissions per byte, co2.js takes into account + // emissions for the user device, the data center and the network. + // Because we already have power tracks showing the power use and estimated + // emissions of the device, set the 'device' grid intensity to 0 to avoid + // double counting. + const co2eq = this._co2!.perByteTrace(bytes, false, { + gridIntensity: { device: 0 }, + }); + const carbonValue = formatNumber( + typeof co2eq.co2 === 'number' ? co2eq.co2 : co2eq.co2.total + ); + const value = formatBytes(bytes); + return ( + + {value} + + ); + } + + _renderTooltip(counterIndex: number): React.ReactNode { + const { + accumulatedSamples, + counter, + rangeStart, + rangeEnd, + interval, + display, + maxCounterSampleCountPerMs, + previewSelection, + } = this.props; + const { mouseX, mouseY } = this.state; + const { samples } = counter; + + if (samples.length === 0) { + throw new Error('No sample found for counter'); + } + + const sampleTime = samples.time[counterIndex]; + if (sampleTime < rangeStart || sampleTime > rangeEnd) { + // Do not draw the tooltip if it will be rendered outside the timeline. + // This could happen when a sample time is outside the time range. + // While range filtering the counters, we add the sample before start and + // after end, so charts will not be cut off at the edges. + return null; + } + + const { unit } = display; + + // Power tooltip — delegate to the dedicated component. + if (unit === 'pWh') { + return ( + + + + ); + } + + // Process CPU tooltip. + if (unit === 'percent') { + 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 (unit === 'bytes' && display.graphType === 'line-rate') { + 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 (unit === 'bytes' && display.graphType === 'line-accumulated') { + 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} +
+
+ ); + } + + // Default tooltip for unknown units — show the raw value. + return ( + +
+
+ + {formatNumber(samples.count[counterIndex])} + + {counter.name} +
+
+
+ ); + } + + /** + * Create a div that is a dot on top of the graph representing the current + * height of the graph. + */ + _renderDot(counterIndex: number): React.ReactNode { + const { + counter, + rangeStart, + rangeEnd, + graphHeight, + width, + lineWidth, + accumulatedSamples, + maxCounterSampleCountPerMs, + interval, + display, + } = this.props; + + const { samples } = counter; + if (samples.length === 0) { + throw new Error('No sample found for counter'); + } + const rangeLength = rangeEnd - rangeStart; + const sampleTime = samples.time[counterIndex]; + + if (sampleTime < rangeStart || sampleTime > rangeEnd) { + // Do not draw the dot if it will be rendered outside the timeline. + // This could happen when a sample time is outside the time range. + // While range filtering the counters, we add the sample before start and + // after end, so charts will not be cut off at the edges. + return null; + } + + const left = (width * (sampleTime - rangeStart)) / rangeLength; + const innerTrackHeight = graphHeight - lineWidth / 2; + let top; + + if (display.graphType === 'line-accumulated') { + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + const unitSampleCount = + (accumulatedCounts[counterIndex] - minCount) / countRange; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + } else { + const sampleTimeDeltaInMs = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + const unitSampleCount = + samples.count[counterIndex] / + sampleTimeDeltaInMs / + maxCounterSampleCountPerMs; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + } + + return ( +
+ ); + } + + override render() { + const { hoveredCounter } = this.state; + const { + filteredThread, + interval, + rangeStart, + rangeEnd, + unfilteredSamplesRange, + counter, + counterSampleRange, + graphHeight, + width, + lineWidth, + accumulatedSamples, + maxCounterSampleCountPerMs, + display, + } = this.props; + + return ( +
+ + {hoveredCounter === null ? null : ( + <> + {this._renderDot(hoveredCounter)} + {this._renderTooltip(hoveredCounter)} + + )} + +
+ ); + } +} + +export const TrackCounterGraph = explicitConnect< + OwnProps, + StateProps, + DispatchProps +>({ + mapStateToProps: (state, ownProps) => { + const { counterIndex } = ownProps; + const counterSelectors = getCounterSelectors(counterIndex); + const counter = counterSelectors.getCounter(state); + const { start, end } = getCommittedRange(state); + const counterSampleRange = + counterSelectors.getCommittedRangeCounterSampleRange(state); + const selectors = getThreadSelectors(counter.mainThreadIndex); + return { + counter, + threadIndex: counter.mainThreadIndex, + accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state), + maxCounterSampleCountPerMs: + counterSelectors.getMaxRangeCounterSampleCountPerMs(state), + rangeStart: start, + rangeEnd: end, + counterSampleRange, + interval: getProfileInterval(state), + filteredThread: selectors.getFilteredThread(state), + unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), + previewSelection: getPreviewSelection(state), + display: counter.display, + }; + }, + component: withSize(TrackCounterGraphImpl), +}); diff --git a/src/components/timeline/TrackMemory.tsx b/src/components/timeline/TrackMemory.tsx deleted file mode 100644 index 7c0dc3cf00..0000000000 --- a/src/components/timeline/TrackMemory.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - getCommittedRange, - getCounterSelectors, -} from 'firefox-profiler/selectors/profile'; -import { TimelineMarkersMemory } from './Markers'; -import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackMemoryGraph } from './TrackMemoryGraph'; -import { - TRACK_MEMORY_GRAPH_HEIGHT, - TRACK_MEMORY_MARKERS_HEIGHT, - TRACK_MEMORY_LINE_WIDTH, -} from 'firefox-profiler/app-logic/constants'; - -import type { - CounterIndex, - ThreadIndex, - Milliseconds, -} from 'firefox-profiler/types'; - -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackMemory.css'; - -type OwnProps = { - readonly counterIndex: CounterIndex; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; -}; - -type DispatchProps = { - updatePreviewSelection: typeof updatePreviewSelection; -}; - -type Props = ConnectedProps; - -type State = {}; - -export class TrackMemoryImpl extends React.PureComponent { - _onMarkerSelect = (start: Milliseconds, end: Milliseconds) => { - const { rangeStart, rangeEnd, updatePreviewSelection } = this.props; - updatePreviewSelection({ - isModifying: false, - selectionStart: Math.max(rangeStart, start), - selectionEnd: Math.min(rangeEnd, end), - }); - }; - - override render() { - const { counterIndex, rangeStart, rangeEnd, threadIndex } = this.props; - return ( -
- - -
- ); - } -} - -export const TrackMemory = explicitConnect( - { - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - return { - threadIndex: counter.mainThreadIndex, - rangeStart: start, - rangeEnd: end, - }; - }, - mapDispatchToProps: { updatePreviewSelection }, - component: TrackMemoryImpl, - } -); diff --git a/src/components/timeline/TrackMemoryGraph.tsx b/src/components/timeline/TrackMemoryGraph.tsx deleted file mode 100644 index 8e1a0f74a6..0000000000 --- a/src/components/timeline/TrackMemoryGraph.tsx +++ /dev/null @@ -1,549 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { InView } from 'react-intersection-observer'; -import { Localized } from '@fluent/react'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { - getStrokeColor, - getFillColor, - getDotColor, -} from 'firefox-profiler/profile-logic/graph-color'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - formatBytes, - formatNumber, -} from 'firefox-profiler/utils/format-numbers'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { TRACK_MEMORY_DEFAULT_COLOR } from 'firefox-profiler/app-logic/constants'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - AccumulatedCounterSamples, - Milliseconds, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackMemory.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackMemoryCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _requestedAnimationFrame: boolean = false; - _canvasState: { renderScheduled: boolean; inView: boolean } = { - renderScheduled: false, - inView: false, - }; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - accumulatedSamples, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - // Take the sample information, and convert it into chart coordinates. Use a slightly - // smaller space than the deviceHeight, so that the stroke will be fully visible - // both at the top and bottom of the chart. - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const [sampleStart, sampleEnd] = counterSampleRange; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // memory graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor( - counter.color || TRACK_MEMORY_DEFAULT_COLOR - ); - ctx.fillStyle = getFillColor(counter.color || TRACK_MEMORY_DEFAULT_COLOR); - ctx.beginPath(); - - // The x and y are used after the loop. - let x = 0; - let y = 0; - let firstX = 0; - for (let i = sampleStart; i < sampleEnd; i++) { - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - x = (samples.time[i] - rangeStart) * millisecondWidth; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange; - y = - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth; - if (i === 0) { - // This is the first iteration, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - firstX = x; - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - } - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _scheduleDraw() { - if (!this._canvasState.inView) { - // Canvas is not in the view. Schedule the render for a later intersection - // observer callback. - this._canvasState.renderScheduled = true; - return; - } - - // Canvas is in the view. Render the canvas and reset the schedule state. - this._canvasState.renderScheduled = false; - - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - const canvas = this._canvas; - if (canvas) { - this.drawCanvas(canvas); - } - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { - this._canvasState.inView = inView; - if (!this._canvasState.renderScheduled) { - // Skip if render is not scheduled. - return; - } - - this._scheduleDraw(); - }; - - override componentDidMount() { - this._scheduleDraw(); - } - - override componentDidUpdate() { - this._scheduleDraw(); - } - - override render() { - return ( - - - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly accumulatedSamples: AccumulatedCounterSamples; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The memory track graph takes memory information from counters, and renders it as a - * graph in the timeline. - */ -class TrackMemoryGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - // This persistTooltips property is part of the web console API. It helps - // in being able to inspect and debug tooltips. - if (window.persistTooltips) { - return; - } - - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - if (counter.samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample group found for memory counter'); - } - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _renderTooltip(counterIndex: number): React.ReactNode { - const { accumulatedSamples, counter, rangeStart, rangeEnd } = this.props; - const { mouseX, mouseY } = this.state; - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No accumulated sample found for memory counter'); - } - - const sampleTime = samples.time[counterIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const bytes = accumulatedCounts[counterIndex] - minCount; - const operations = - samples.number !== undefined ? samples.number[counterIndex] : null; - return ( - -
-
- - {formatBytes(bytes)} - - - relative memory at this time - -
- -
- - {formatBytes(countRange)} - - - memory range in graph - -
- {operations !== null ? ( -
- - {formatNumber(operations, 2, 0)} - - - allocations and deallocations since the previous sample - -
- ) : null} -
-
- ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderMemoryDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - accumulatedSamples, - } = this.props; - - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for memory counter'); - } - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = (width * (sampleTime - rangeStart)) / rangeLength; - - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const unitSampleCount = - (accumulatedCounts[counterIndex] - minCount) / countRange; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - accumulatedSamples, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderMemoryDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackMemoryGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - }; - }, - component: withSize(TrackMemoryGraphImpl), -}); diff --git a/src/components/timeline/TrackPower.css b/src/components/timeline/TrackPower.css deleted file mode 100644 index 09f616a1c2..0000000000 --- a/src/components/timeline/TrackPower.css +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.timelineTrackPowerGraph { - position: relative; - width: 100%; - height: var(--graph-height); -} - -.timelineTrackPowerCanvas { - position: absolute; - width: 100%; - height: 100%; -} - -.timelineTrackPowerGraphDot { - position: absolute; - width: 6px; - height: 6px; - border-radius: 3px; - margin-top: -3px; - margin-left: -3px; - pointer-events: none; -} diff --git a/src/components/timeline/TrackPower.tsx b/src/components/timeline/TrackPower.tsx deleted file mode 100644 index 3272f09c19..0000000000 --- a/src/components/timeline/TrackPower.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { - getCommittedRange, - getCounterSelectors, -} from 'firefox-profiler/selectors/profile'; -import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackPowerGraph } from './TrackPowerGraph'; -import { - TRACK_POWER_HEIGHT, - TRACK_POWER_LINE_WIDTH, -} from 'firefox-profiler/app-logic/constants'; - -import type { - CounterIndex, - ThreadIndex, - Milliseconds, -} from 'firefox-profiler/types'; - -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackPower.css'; - -type OwnProps = { - readonly counterIndex: CounterIndex; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; -}; - -type DispatchProps = { - updatePreviewSelection: typeof updatePreviewSelection; -}; - -type Props = ConnectedProps; - -type State = {}; - -export class TrackPowerImpl extends React.PureComponent { - override render() { - const { counterIndex } = this.props; - return ( -
- -
- ); - } -} - -export const TrackPower = explicitConnect({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - return { - threadIndex: counter.mainThreadIndex, - rangeStart: start, - rangeEnd: end, - }; - }, - mapDispatchToProps: { updatePreviewSelection }, - component: TrackPowerImpl, -}); diff --git a/src/components/timeline/TrackPowerGraph.tsx b/src/components/timeline/TrackPowerGraph.tsx deleted file mode 100644 index c8613636f5..0000000000 --- a/src/components/timeline/TrackPowerGraph.tsx +++ /dev/null @@ -1,574 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { InView } from 'react-intersection-observer'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { - getStrokeColor, - getFillColor, - getDotColor, -} from 'firefox-profiler/profile-logic/graph-color'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { TooltipTrackPower } from 'firefox-profiler/components/tooltip/TrackPower'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { TRACK_POWER_DEFAULT_COLOR } from 'firefox-profiler/app-logic/constants'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - Milliseconds, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; -import { timeCode } from 'firefox-profiler/utils/time-code'; - -import './TrackPower.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackPowerCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _canvasState: { renderScheduled: boolean; inView: boolean } = { - renderScheduled: false, - inView: false, - }; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - maxCounterSampleCountPerMs, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - const [sampleStart, sampleEnd] = counterSampleRange; - const countRangePerMs = maxCounterSampleCountPerMs; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // power graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor( - counter.color || TRACK_POWER_DEFAULT_COLOR - ); - ctx.fillStyle = getFillColor(counter.color || TRACK_POWER_DEFAULT_COLOR); - ctx.beginPath(); - - const getX = (i: number) => - Math.round((samples.time[i] - rangeStart) * millisecondWidth); - const getPower = (i: number) => { - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - return samples.count[i] / sampleTimeDeltaInMs; - }; - const getY = (rawY: number) => { - if (!rawY) { - // Make the 0 values invisible so that 'almost 0' is noticeable. - return deviceHeight + deviceLineHalfWidth; - } - - const unitGraphCount = rawY / countRangePerMs; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - return Math.round( - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth - ); - }; - - // The x and y are used after the loop. - const firstX = getX(sampleStart); - let x = firstX; - let y = getY(getPower(sampleStart)); - - // For the first sample, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - ctx.moveTo(x, y); - - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - for (let i = sampleStart + 1; i < sampleEnd; i++) { - const powerValues = [getPower(i)]; - x = getX(i); - y = getY(powerValues[0]); - ctx.lineTo(x, y); - - // If we have multiple samples to draw on the same horizontal pixel, - // we process all of them together with a max-min decimation algorithm - // to save time: - // - We draw the first and last samples to ensure the display is - // correct if there are sampling gaps. - // - For the values in between, we only draw the min and max values, - // to draw a vertical line covering all the other sample values. - while (i + 1 < sampleEnd && getX(i + 1) === x) { - powerValues.push(getPower(++i)); - } - - // Looking for the min and max only makes sense if we have more than 2 - // samples to draw. - if (powerValues.length > 2) { - const minY = getY(Math.min(...powerValues)); - if (minY !== y) { - y = minY; - ctx.lineTo(x, y); - } - const maxY = getY(Math.max(...powerValues)); - if (maxY !== y) { - y = maxY; - ctx.lineTo(x, y); - } - } - - const lastY = getY(powerValues[powerValues.length - 1]); - if (lastY !== y) { - y = lastY; - ctx.lineTo(x, y); - } - } - - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _renderCanvas() { - if (!this._canvasState.inView) { - // Canvas is not in the view. Schedule the render for a later intersection - // observer callback. - this._canvasState.renderScheduled = true; - return; - } - - // Canvas is in the view. Render the canvas and reset the schedule state. - this._canvasState.renderScheduled = false; - - const canvas = this._canvas; - if (canvas) { - timeCode('TrackPowerCanvas render', () => { - this.drawCanvas(canvas); - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - _observerCallback = (inView: boolean, _entry: IntersectionObserverEntry) => { - this._canvasState.inView = inView; - if (!this._canvasState.renderScheduled) { - // Skip if render is not scheduled. - return; - } - - this._renderCanvas(); - }; - - override render() { - this._renderCanvas(); - - return ( - - - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The power track graph takes power use information from counters, and renders it as a - * graph in the timeline. - */ -class TrackPowerGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - - // If there are samples before or after hoveredCounter that fall - // horizontally on the same pixel, move hoveredCounter to the sample - // with the highest power value. - const mouseAtTime = (t: number) => - Math.round(((t - rangeStart) / rangeLength) * width + left); - for ( - let currentIndex = hoveredCounter - 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex > 0; - --currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - for ( - let currentIndex = hoveredCounter + 1; - mouseAtTime(samples.time[currentIndex]) === mouseX && - currentIndex < samples.time.length; - ++currentIndex - ) { - if (samples.count[currentIndex] > samples.count[hoveredCounter]) { - hoveredCounter = currentIndex; - } - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _renderTooltip(counterSampleIndex: number): React.ReactNode { - const { counter, rangeStart, rangeEnd } = this.props; - const { mouseX, mouseY } = this.state; - - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for power counter'); - } - - const sampleTime = samples.time[counterSampleIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - return ( - - - - ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - interval, - } = this.props; - - const { samples } = counter; - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = (width * (sampleTime - rangeStart)) / rangeLength; - - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // power counter. Print an error and bail out early. - throw new Error('No sample found for power counter'); - } - const countRangePerMs = maxCounterSampleCountPerMs; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / sampleTimeDeltaInMs / countRangePerMs; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackPowerGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - maxCounterSampleCountPerMs: - counterSelectors.getMaxRangeCounterSampleCountPerMs(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - }; - }, - component: withSize(TrackPowerGraphImpl), -}); diff --git a/src/components/timeline/TrackProcessCPU.css b/src/components/timeline/TrackProcessCPU.css deleted file mode 100644 index 76f8c45d52..0000000000 --- a/src/components/timeline/TrackProcessCPU.css +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.timelineTrackProcessCPUGraph { - position: relative; - width: 100%; - height: var(--graph-height); -} - -.timelineTrackProcessCPUCanvas { - position: absolute; - width: 100%; - height: 100%; -} - -.timelineTrackProcessCPUGraphDot { - --internal-background-color: var(--grey-50); - - position: absolute; - width: 6px; - height: 6px; - border-radius: 3px; - margin-top: -3px; - margin-left: -3px; - background-color: var(--internal-background-color); - pointer-events: none; -} - -.timelineTrackProcessCPUTooltipLine { - white-space: nowrap; -} - -.timelineTrackProcessCPUTooltipNumber { - display: inline-block; - min-width: 60px; - color: var(--tooltip-number-foreground-color); - font-weight: bold; -} diff --git a/src/components/timeline/TrackProcessCPUGraph.tsx b/src/components/timeline/TrackProcessCPUGraph.tsx deleted file mode 100644 index 0f6c7d7fe3..0000000000 --- a/src/components/timeline/TrackProcessCPUGraph.tsx +++ /dev/null @@ -1,477 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import * as React from 'react'; -import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import explicitConnect from 'firefox-profiler/utils/connect'; -import { formatPercent } from 'firefox-profiler/utils/format-numbers'; -import { bisectionRight } from 'firefox-profiler/utils/bisect'; -import { - getCommittedRange, - getCounterSelectors, - getProfileInterval, -} from 'firefox-profiler/selectors/profile'; -import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { GREY_50 } from 'photon-colors'; -import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; -import { EmptyThreadIndicator } from './EmptyThreadIndicator'; - -import type { - CounterIndex, - Counter, - Thread, - ThreadIndex, - Milliseconds, - CssPixels, - StartEndRange, - IndexIntoSamplesTable, -} from 'firefox-profiler/types'; - -import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; - -import './TrackProcessCPU.css'; - -/** - * When adding properties to these props, please consider the comment above the component. - */ -type CanvasProps = { - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly width: CssPixels; - readonly height: CssPixels; - readonly lineWidth: CssPixels; -}; - -/** - * This component controls the rendering of the canvas. Every render call through - * React triggers a new canvas render. Because of this, it's important to only pass - * in the props that are needed for the canvas draw call. - */ -class TrackProcessCPUCanvas extends React.PureComponent { - _canvas: null | HTMLCanvasElement = null; - _requestedAnimationFrame: boolean = false; - - drawCanvas(canvas: HTMLCanvasElement): void { - const { - rangeStart, - rangeEnd, - counter, - height, - width, - lineWidth, - interval, - maxCounterSampleCountPerMs, - counterSampleRange, - } = this.props; - if (width === 0) { - // This is attempting to draw before the canvas was laid out. - return; - } - - const ctx = canvas.getContext('2d')!; - const devicePixelRatio = window.devicePixelRatio; - const deviceWidth = width * devicePixelRatio; - const deviceHeight = height * devicePixelRatio; - const deviceLineWidth = lineWidth * devicePixelRatio; - const deviceLineHalfWidth = deviceLineWidth * 0.5; - const innerDeviceHeight = deviceHeight - deviceLineWidth; - const rangeLength = rangeEnd - rangeStart; - const millisecondWidth = deviceWidth / rangeLength; - const intervalWidth = interval * millisecondWidth; - - // Resize and clear the canvas. - canvas.width = Math.round(deviceWidth); - canvas.height = Math.round(deviceHeight); - ctx.clearRect(0, 0, deviceWidth, deviceHeight); - - const samples = counter.samples; - if (samples.length === 0) { - // There's no reason to draw the samples, there are none. - return; - } - - const [sampleStart, sampleEnd] = counterSampleRange; - const countRangePerMs = maxCounterSampleCountPerMs; - - { - // Draw the chart. - // - // ...--` - // 1 ...---```..-- `--. 2 - // |_____________________| - // 4 3 - // - // Start by drawing from 1 - 2. This will be the top of all the peaks of the - // process CPU graph. - - ctx.lineWidth = deviceLineWidth; - ctx.lineJoin = 'bevel'; - ctx.strokeStyle = GREY_50; - ctx.fillStyle = '#73737388'; // Grey 50 with transparency. - ctx.beginPath(); - - // The x and y are used after the loop. - let x = 0; - let y = 0; - let firstX = 0; - for (let i = sampleStart; i < sampleEnd; i++) { - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - x = (samples.time[i] - rangeStart) * millisecondWidth; - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - const unitGraphCount = - samples.count[i] / sampleTimeDeltaInMs / countRangePerMs; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - y = - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth; - if (i === 0) { - // This is the first iteration, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - firstX = x; - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - } - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); - - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); - - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. - - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); - - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); - - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } - } - - _scheduleDraw() { - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - const canvas = this._canvas; - if (canvas) { - this.drawCanvas(canvas); - } - }); - } - } - - _takeCanvasRef = (canvas: HTMLCanvasElement | null) => { - this._canvas = canvas; - }; - - override render() { - this._scheduleDraw(); - - return ( - - ); - } -} - -type OwnProps = { - readonly counterIndex: CounterIndex; - readonly lineWidth: CssPixels; - readonly graphHeight: CssPixels; -}; - -type StateProps = { - readonly threadIndex: ThreadIndex; - readonly rangeStart: Milliseconds; - readonly rangeEnd: Milliseconds; - readonly counter: Counter; - readonly counterSampleRange: [IndexIntoSamplesTable, IndexIntoSamplesTable]; - readonly maxCounterSampleCountPerMs: number; - readonly interval: Milliseconds; - readonly filteredThread: Thread; - readonly unfilteredSamplesRange: StartEndRange | null; -}; - -type DispatchProps = {}; - -type Props = SizeProps & ConnectedProps; - -type State = { - hoveredCounter: null | number; - mouseX: CssPixels; - mouseY: CssPixels; -}; - -/** - * The process CPU track graph takes CPU information from counters, and renders it as a - * graph in the timeline. - */ -class TrackProcessCPUGraphImpl extends React.PureComponent { - override state = { - hoveredCounter: null, - mouseX: 0, - mouseY: 0, - }; - - _onMouseLeave = () => { - this.setState({ hoveredCounter: null }); - }; - - _onMouseMove = (event: React.MouseEvent) => { - const { pageX: mouseX, pageY: mouseY } = event; - // Get the offset from here, and apply it to the time lookup. - const { left } = event.currentTarget.getBoundingClientRect(); - const { - width, - rangeStart, - rangeEnd, - counter, - interval, - counterSampleRange, - } = this.props; - const rangeLength = rangeEnd - rangeStart; - const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength; - - const { samples } = counter; - - if ( - timeAtMouse < samples.time[0] || - timeAtMouse > samples.time[samples.length - 1] + interval - ) { - // We are outside the range of the samples, do not display hover information. - this.setState({ hoveredCounter: null }); - } else { - // When the mouse pointer hovers between two points, select the point that's closer. - let hoveredCounter; - const [sampleStart, sampleEnd] = counterSampleRange; - const bisectionCounter = bisectionRight( - samples.time, - timeAtMouse, - sampleStart, - sampleEnd - ); - if (bisectionCounter > 0 && bisectionCounter < samples.time.length) { - const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1]; - const rightDistance = samples.time[bisectionCounter] - timeAtMouse; - if (leftDistance < rightDistance) { - // Left point is closer - hoveredCounter = bisectionCounter - 1; - } else { - // Right point is closer - hoveredCounter = bisectionCounter; - } - } else { - hoveredCounter = bisectionCounter; - } - - if (hoveredCounter === samples.length) { - // When hovering the last sample, it's possible the mouse is past the time. - // In this case, hover over the last sample. This happens because of the - // ` + interval` line in the `if` condition above. - hoveredCounter = samples.time.length - 1; - } - - this.setState({ - mouseX, - mouseY, - hoveredCounter, - }); - } - }; - - _renderTooltip(counterIndex: number): React.ReactNode { - const { - counter, - maxCounterSampleCountPerMs, - interval, - rangeStart, - rangeEnd, - } = this.props; - const { mouseX, mouseY } = this.state; - const { samples } = counter; - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // malloc counter. Print an error and bail out early. - throw new Error('No sample found for process CPU counter'); - } - const sampleTime = samples.time[counterIndex]; - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the tooltip if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const maxCPUPerMs = maxCounterSampleCountPerMs; - const cpuUsage = samples.count[counterIndex]; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const cpuRatio = cpuUsage / sampleTimeDeltaInMs / maxCPUPerMs; - return ( - -
-
- CPU:{' '} - - {formatPercent(cpuRatio)} - -
-
-
- ); - } - - /** - * Create a div that is a dot on top of the graph representing the current - * height of the graph. - */ - _renderDot(counterIndex: number): React.ReactNode { - const { - counter, - rangeStart, - rangeEnd, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - interval, - } = this.props; - const { samples } = counter; - const rangeLength = rangeEnd - rangeStart; - const sampleTime = samples.time[counterIndex]; - - if (sampleTime < rangeStart || sampleTime > rangeEnd) { - // Do not draw the dot if it will be rendered outside of the timeline. - // This could happen when a sample time is outside of the time range. - // While range filtering the counters, we add the sample before start and - // after end, so charts will not be cut off at the edges. - return null; - } - - const left = - (width * (samples.time[counterIndex] - rangeStart)) / rangeLength; - - if (samples.length === 0) { - // Gecko failed to capture samples for some reason and it shouldn't happen for - // process CPU counter. Print an error and bail out early. - throw new Error('No sample found for process CPU counter'); - } - const countRangePerMs = maxCounterSampleCountPerMs; - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / sampleTimeDeltaInMs / countRangePerMs; - const innerTrackHeight = graphHeight - lineWidth / 2; - const top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - - return ( -
- ); - } - - override render() { - const { hoveredCounter } = this.state; - const { - filteredThread, - interval, - rangeStart, - rangeEnd, - unfilteredSamplesRange, - counter, - counterSampleRange, - graphHeight, - width, - lineWidth, - maxCounterSampleCountPerMs, - } = this.props; - - return ( -
- - {hoveredCounter === null ? null : ( - <> - {this._renderDot(hoveredCounter)} - {this._renderTooltip(hoveredCounter)} - - )} - -
- ); - } -} - -export const TrackProcessCPUGraph = explicitConnect< - OwnProps, - StateProps, - DispatchProps ->({ - mapStateToProps: (state, ownProps) => { - const { counterIndex } = ownProps; - const counterSelectors = getCounterSelectors(counterIndex); - const counter = counterSelectors.getCounter(state); - const { start, end } = getCommittedRange(state); - const counterSampleRange = - counterSelectors.getCommittedRangeCounterSampleRange(state); - const selectors = getThreadSelectors(counter.mainThreadIndex); - return { - counter, - threadIndex: counter.mainThreadIndex, - maxCounterSampleCountPerMs: - counterSelectors.getMaxCounterSampleCountPerMs(state), - rangeStart: start, - rangeEnd: end, - counterSampleRange, - interval: getProfileInterval(state), - filteredThread: selectors.getFilteredThread(state), - unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), - }; - }, - component: withSize(TrackProcessCPUGraphImpl), -}); diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index d95355d18d..c760201465 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -1013,7 +1013,7 @@ function _deriveCounterDisplay( unit: 'percent', color: 'grey', markerSchemaLocation: null, - sortWeight: 40, + sortWeight: 70, label: 'Process CPU', }; } @@ -1023,7 +1023,7 @@ function _deriveCounterDisplay( unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 50, + sortWeight: 35, label: name, }; } diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index c6b48dec51..e44eeb00a2 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3092,7 +3092,7 @@ const _upgraders: { unit: 'percent', color: 'grey', markerSchemaLocation: null, - sortWeight: 40, + sortWeight: 70, label: 'Process CPU', }; } else { @@ -3101,7 +3101,7 @@ const _upgraders: { unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 50, + sortWeight: 35, label: name, }; } diff --git a/src/profile-logic/tracks.ts b/src/profile-logic/tracks.ts index c831ad796e..4e61ddf608 100644 --- a/src/profile-logic/tracks.ts +++ b/src/profile-logic/tracks.ts @@ -56,30 +56,49 @@ export type HiddenTracks = { const LOCAL_TRACK_INDEX_ORDER = { thread: 0, network: 1, - memory: 2, + // Needed only for ts types; counter tracks use _getCounterTrackIndexOrder(). + counter: -1, ipc: 3, 'event-delay': 4, - 'process-cpu': 5, - power: 6, marker: 7, - bandwidth: 8, }; const LOCAL_TRACK_DISPLAY_ORDER = { network: 0, - bandwidth: 1, - memory: 2, - power: 3, + // Needed only for ts types; counter tracks use display.sortWeight. + counter: -1, // IPC tracks that belong to the global track will appear right after network // and counter tracks. But we want to show the IPC tracks that belong to the // local threads right after their track. This special handling happens inside // the sort function. - ipc: 4, - thread: 5, - 'event-delay': 6, - 'process-cpu': 7, - marker: 8, + ipc: 40, + thread: 50, + 'event-delay': 60, + marker: 80, }; +/** + * Map a counter's category and name to the old LOCAL_TRACK_INDEX_ORDER value + * that was used before all counter types were unified. This is needed for + * backward compatibility of URL-encoded track indexes. + */ +function _getCounterTrackIndexOrder(counter: RawCounter): number { + const { category, name } = counter; + if (category === 'Memory') { + return 2; + } + if (category === 'CPU' && name === 'processCPU') { + return 5; + } + if (category === 'power') { + return 6; + } + if (category === 'Bandwidth') { + return 8; + } + // Unknown counter types go after all known types. + return 9; +} + const GLOBAL_TRACK_INDEX_ORDER = { process: 0, screenshots: 1, @@ -126,17 +145,22 @@ function _getDefaultLocalTrackOrder( if ( profile && profile.counters && - tracks[a].type === 'power' && - tracks[b].type === 'power' + tracks[a].type === 'counter' && + tracks[b].type === 'counter' ) { - const idxA = tracks[a].counterIndex; - const idxB = tracks[b].counterIndex; + const counterA = profile.counters[tracks[a].counterIndex]; + const counterB = profile.counters[tracks[b].counterIndex]; + // Sort counter tracks by their display.sortWeight first. + const sortWeightDiff = + counterA.display.sortWeight - counterB.display.sortWeight; + if (sortWeightDiff !== 0) { + return sortWeightDiff; + } + // Within the same sortWeight, sort by name. if (profile.meta.keepProfileThreadOrder) { - return idxA - idxB; + return tracks[a].counterIndex - tracks[b].counterIndex; } - const nameA = profile.counters[idxA].name; - const nameB = profile.counters[idxB].name; - return naturalSort.compare(nameA, nameB); + return naturalSort.compare(counterA.name, counterB.name); } // If the tracks are both threads, sort them by thread name, and then by @@ -158,10 +182,15 @@ function _getDefaultLocalTrackOrder( ); } - return ( - LOCAL_TRACK_DISPLAY_ORDER[tracks[a].type] - - LOCAL_TRACK_DISPLAY_ORDER[tracks[b].type] - ); + const displayA = + tracks[a].type === 'counter' && profile && profile.counters + ? profile.counters[tracks[a].counterIndex].display.sortWeight + : LOCAL_TRACK_DISPLAY_ORDER[tracks[a].type]; + const displayB = + tracks[b].type === 'counter' && profile && profile.counters + ? profile.counters[tracks[b].counterIndex].display.sortWeight + : LOCAL_TRACK_DISPLAY_ORDER[tracks[b].type]; + return displayA - displayB; }); return trackOrder; @@ -400,30 +429,29 @@ export function computeLocalTracksByPid( const { counters } = profile; if (counters) { for (let counterIndex = 0; counterIndex < counters.length; counterIndex++) { - const { pid, category, samples } = counters[counterIndex]; + const { pid, category, name, samples } = counters[counterIndex]; if (!availablePids.has(pid)) { // If the global track is filtered out ignore it here too. continue; } - if (['Memory', 'power', 'Bandwidth'].includes(category)) { - if (category === 'power' && samples.length <= 2) { - // If we have only 2 samples, they are likely both 0 and we don't have a real counter. - continue; - } - let tracks = localTracksByPid.get(pid); - if (tracks === undefined) { - tracks = []; - localTracksByPid.set(pid, tracks); - } - if (category === 'Memory') { - tracks.push({ type: 'memory', counterIndex }); - } else if (category === 'Bandwidth') { - tracks.push({ type: 'bandwidth', counterIndex }); - } else { - tracks.push({ type: 'power', counterIndex }); - } + // Skip processCPU counters — they are added separately by + // addProcessCPUTracksForProcess when the experimental flag is enabled. + if (category === 'CPU' && name === 'processCPU') { + continue; + } + + if (category === 'power' && samples.length <= 2) { + // If we have only 2 samples, they are likely both 0 and we don't have a real counter. + continue; + } + + let tracks = localTracksByPid.get(pid); + if (tracks === undefined) { + tracks = []; + localTracksByPid.set(pid, tracks); } + tracks.push({ type: 'counter', counterIndex }); } } @@ -431,10 +459,17 @@ export function computeLocalTracksByPid( // added at the end so that the local track indexes are stable and backwards compatible. for (const localTracks of localTracksByPid.values()) { // In place sort! - localTracks.sort( - (a: LocalTrack, b: LocalTrack) => - LOCAL_TRACK_INDEX_ORDER[a.type] - LOCAL_TRACK_INDEX_ORDER[b.type] - ); + localTracks.sort((a: LocalTrack, b: LocalTrack) => { + const orderA = + a.type === 'counter' && counters + ? _getCounterTrackIndexOrder(counters[a.counterIndex]) + : LOCAL_TRACK_INDEX_ORDER[a.type]; + const orderB = + b.type === 'counter' && counters + ? _getCounterTrackIndexOrder(counters[b.counterIndex]) + : LOCAL_TRACK_INDEX_ORDER[b.type]; + return orderA - orderB; + }); } return localTracksByPid; @@ -496,7 +531,7 @@ export function addProcessCPUTracksForProcess( let localTracks = newLocalTracksByPid.get(pid) ?? []; // Do not mutate the current state. - localTracks = [...localTracks, { type: 'process-cpu', counterIndex }]; + localTracks = [...localTracks, { type: 'counter', counterIndex }]; newLocalTracksByPid.set(pid, localTracks); } @@ -1116,10 +1151,10 @@ export function getLocalTrackName( return getFriendlyThreadName(threads, threads[localTrack.threadIndex]); case 'network': return 'Network'; - case 'memory': - return 'Memory'; - case 'bandwidth': - return 'Bandwidth'; + case 'counter': { + const counter = counters[localTrack.counterIndex]; + return counter.display.label || counter.name; + } case 'ipc': return `IPC — ${getFriendlyThreadName( threads, @@ -1130,10 +1165,6 @@ export function getLocalTrackName( getFriendlyThreadName(threads, threads[localTrack.threadIndex]) + ' Event Delay' ); - case 'process-cpu': - return 'Process CPU'; - case 'power': - return counters[localTrack.counterIndex].name; case 'marker': return shared.stringArray[localTrack.markerName]; default: @@ -1577,14 +1608,20 @@ export function getSearchFilteredLocalTracksByPid( } break; } + case 'counter': { + // Match against the counter's display label (e.g., "Memory", + // "Bandwidth") rather than the generic type string 'counter'. + const trackName = localTrackNames[trackIndex]; + if (searchRegExp.test(trackName)) { + searchFilteredLocalTracks.add(trackIndex); + continue; + } + break; + } case 'network': - case 'memory': - case 'bandwidth': case 'marker': case 'ipc': - case 'event-delay': - case 'power': - case 'process-cpu': { + case 'event-delay': { const { type } = localTrack; if (searchRegExp.test(type)) { searchFilteredLocalTracks.add(trackIndex); @@ -1761,8 +1798,6 @@ export function getTrackReferenceFromThreadIndex( * If the track is not a thread, some of them can be visible by default and some * of them can be hidden to reduce the noise. This mostly depends on either the * usefulness or the activity of that track. - * - * TODO: Check the memory track activity here to decide if it should be visible. */ function _isLocalTrackVisible( localTrack: LocalTrack, @@ -1774,15 +1809,10 @@ function _isLocalTrackVisible( return visibleThreadIndexes.has(localTrack.threadIndex); case 'marker': case 'network': - case 'memory': - case 'bandwidth': - // 'event-delay' and 'process-cpu' tracks are experimental and they should - // be visible by default whenever they are included in a profile. (fallthrough) + case 'counter': + // 'event-delay' track is experimental, and it should be visible by default + // whenever it is included in a profile. (fallthrough) case 'event-delay': - case 'process-cpu': - // Power tracks are there only if the power feature is enabled. So they should - // be visible by default whenever they're included in a profile. (fallthrough) - case 'power': // Keep non-thread local tracks visible. return true; case 'ipc': diff --git a/src/selectors/app.tsx b/src/selectors/app.tsx index d46d71eb4e..1224f702ad 100644 --- a/src/selectors/app.tsx +++ b/src/selectors/app.tsx @@ -9,21 +9,20 @@ import { getHiddenGlobalTracks, getHiddenLocalTracksByPid, } from './url-state'; -import { getGlobalTracks, getLocalTracksByPid } from './profile'; +import { getGlobalTracks, getLocalTracksByPid, getCounters } from './profile'; import { getZipFileState } from './zipped-profiles'; import { assertExhaustiveCheck, ensureExists } from '../utils/types'; import { FULL_TRACK_SCREENSHOT_HEIGHT, TRACK_NETWORK_HEIGHT, - TRACK_MEMORY_HEIGHT, - TRACK_BANDWIDTH_HEIGHT, TRACK_IPC_HEIGHT, TRACK_PROCESS_BLANK_HEIGHT, TIMELINE_RULER_HEIGHT, TRACK_VISUAL_PROGRESS_HEIGHT, TRACK_EVENT_DELAY_HEIGHT, - TRACK_PROCESS_CPU_HEIGHT, TRACK_MARKER_HEIGHT, + TRACK_COUNTER_GRAPH_HEIGHT, + TRACK_COUNTER_MARKERS_HEIGHT, } from '../app-logic/constants'; import type { @@ -106,12 +105,14 @@ export const getTimelineHeight: Selector = createSelector( getHiddenGlobalTracks, getHiddenLocalTracksByPid, getTrackThreadHeights, + getCounters, ( globalTracks, localTracksByPid, hiddenGlobalTracks, hiddenLocalTracksByPid, - trackThreadHeights + trackThreadHeights, + counters ) => { let height = TIMELINE_RULER_HEIGHT; const border = 1; @@ -186,22 +187,24 @@ export const getTimelineHeight: Selector = createSelector( case 'network': height += TRACK_NETWORK_HEIGHT + border; break; - case 'memory': - height += TRACK_MEMORY_HEIGHT + border; - break; - case 'bandwidth': - height += TRACK_BANDWIDTH_HEIGHT + border; + case 'counter': { + // Counter track height depends on whether it has markers. + const counter = counters?.[localTrack.counterIndex]; + const hasMarkers = + counter?.display.markerSchemaLocation !== null && + counter?.display.markerSchemaLocation !== undefined; + height += hasMarkers + ? TRACK_COUNTER_GRAPH_HEIGHT + TRACK_COUNTER_MARKERS_HEIGHT + : TRACK_COUNTER_GRAPH_HEIGHT; + height += border; break; + } case 'event-delay': height += TRACK_EVENT_DELAY_HEIGHT + border; break; case 'ipc': height += TRACK_IPC_HEIGHT + border; break; - case 'process-cpu': - case 'power': - height += TRACK_PROCESS_CPU_HEIGHT + border; - break; case 'marker': height += TRACK_MARKER_HEIGHT + border; break; diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 7ac0ef4f2c..9501c18fda 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -549,10 +549,19 @@ export const getLocalTrackFromReference: DangerousSelectorWithArguments< */ export const getProcessesWithMemoryTrack: Selector> = createSelector( getLocalTracksByPid, - (localTracksByPid) => { + getCounters, + (localTracksByPid, counters) => { const processesWithMemoryTrack = new Set(); for (const [pid, localTracks] of localTracksByPid.entries()) { - if (localTracks.some((track) => track.type === 'memory')) { + if ( + localTracks.some( + (track) => + track.type === 'counter' && + counters !== null && + counters[track.counterIndex].display.markerSchemaLocation === + 'timeline-memory' + ) + ) { processesWithMemoryTrack.add(pid); } } diff --git a/src/test/components/TrackBandwidth.test.tsx b/src/test/components/TrackBandwidth.test.tsx index b8109c8693..84de06ef60 100644 --- a/src/test/components/TrackBandwidth.test.tsx +++ b/src/test/components/TrackBandwidth.test.tsx @@ -13,7 +13,7 @@ import { } from 'firefox-profiler/test/fixtures/testing-library'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackBandwidth } from '../../components/timeline/TrackBandwidth'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -66,30 +66,37 @@ describe('TrackBandwidth', function () { for (let i = 7; i < thread.samples.length - 1; ++i) { sampleTimes[i] = 7 + i / 100; } - profile.counters = [ - getCounterForThreadWithSamples( - thread, - threadIndex, - { - time: sampleTimes.slice(), - // Bandwidth usage numbers. They are bytes. - count: [ - 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, - 20000, 1, 12000, 100000, - ], - length: SAMPLE_COUNT, - }, - 'SystemBandwidth', - 'bandwidth' - ), - ]; + const counter = getCounterForThreadWithSamples( + thread, + threadIndex, + { + time: sampleTimes.slice(), + // Bandwidth usage numbers. They are bytes. + count: [ + 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, 20000, + 1, 12000, 100000, + ], + length: SAMPLE_COUNT, + }, + 'SystemBandwidth', + 'bandwidth' + ); + counter.display = { + ...counter.display, + graphType: 'line-rate', + unit: 'bytes', + color: 'blue', + sortWeight: 10, + label: 'Bandwidth', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); const renderResult = render( - + ); const { container } = renderResult; @@ -98,13 +105,13 @@ describe('TrackBandwidth', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackBandwidthCanvas'), - `Couldn't find the bandwidth canvas, with selector .timelineTrackBandwidthCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the bandwidth canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackBandwidthTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getBandwidthDot = () => - container.querySelector('.timelineTrackBandwidthGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, diff --git a/src/test/components/TrackMemory.test.tsx b/src/test/components/TrackMemory.test.tsx index ab31d78e21..757293cd48 100644 --- a/src/test/components/TrackMemory.test.tsx +++ b/src/test/components/TrackMemory.test.tsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import { render } from 'firefox-profiler/test/fixtures/testing-library'; -import { TrackMemory } from '../../components/timeline/TrackMemory'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -63,16 +63,25 @@ describe('TrackMemory', function () { ); const threadIndex = 0; const thread = profile.threads[threadIndex]; - profile.counters = [ - getCounterForThread(thread, threadIndex, counterConfig), - ]; + const counter = getCounterForThread(thread, threadIndex, counterConfig); + counter.category = 'Memory'; + counter.display = { + ...counter.display, + graphType: 'line-accumulated', + unit: 'bytes', + color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); const renderResult = render( - + ); const { container } = renderResult; @@ -81,13 +90,13 @@ describe('TrackMemory', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackMemoryCanvas'), - `Couldn't find the memory canvas, with selector .timelineTrackMemoryCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the memory canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackMemoryTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getMemoryDot = () => - container.querySelector('.timelineTrackMemoryGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, @@ -194,7 +203,7 @@ describe('TrackMemory with intersection observer', function () { const renderResult = render( - + ); diff --git a/src/test/components/TrackPower.test.tsx b/src/test/components/TrackPower.test.tsx index 402180cec9..c952a33f35 100644 --- a/src/test/components/TrackPower.test.tsx +++ b/src/test/components/TrackPower.test.tsx @@ -9,7 +9,7 @@ import { fireEvent } from '@testing-library/react'; import { render, screen } from 'firefox-profiler/test/fixtures/testing-library'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; -import { TrackPower } from '../../components/timeline/TrackPower'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -62,30 +62,36 @@ describe('TrackPower', function () { for (let i = 7; i < sampleTimes.length - 1; ++i) { sampleTimes[i] = 7 + i / 100; } - profile.counters = [ - getCounterForThreadWithSamples( - thread, - threadIndex, - { - time: sampleTimes.slice(), - // Power usage numbers. They are pWh so they are pretty big. - count: [ - 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, - 20000, 1, 12000, 100000, - ], - length: SAMPLE_COUNT, - }, - 'SystemPower', - 'power' - ), - ]; + const counter = getCounterForThreadWithSamples( + thread, + threadIndex, + { + time: sampleTimes.slice(), + // Power usage numbers. They are pWh so they are pretty big. + count: [ + 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, 20000, + 1, 12000, 100000, + ], + length: SAMPLE_COUNT, + }, + 'SystemPower', + 'power' + ); + counter.display = { + ...counter.display, + graphType: 'line-rate', + unit: 'pWh', + sortWeight: 30, + label: 'SystemPower', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); const renderResult = render( - + ); const { container } = renderResult; @@ -94,13 +100,13 @@ describe('TrackPower', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackPowerCanvas'), - `Couldn't find the power canvas, with selector .timelineTrackPowerCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the power canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => document.querySelector('.timelineTrackPowerTooltip'); const getPowerDot = () => - container.querySelector('.timelineTrackPowerGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, diff --git a/src/test/components/TrackProcessCPU.test.tsx b/src/test/components/TrackProcessCPU.test.tsx index 0fc742f5e4..9772159547 100644 --- a/src/test/components/TrackProcessCPU.test.tsx +++ b/src/test/components/TrackProcessCPU.test.tsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import { render, screen } from 'firefox-profiler/test/fixtures/testing-library'; -import { TrackProcessCPU } from '../../components/timeline/TrackProcessCPU'; +import { TrackCounter } from '../../components/timeline/TrackCounter'; import { ensureExists } from '../../utils/types'; import { @@ -55,27 +55,33 @@ describe('TrackProcessCPU', function () { const sampleTimes = ensureExists(thread.samples.time); // Changing one of the sample times, so we can test different intervals. sampleTimes[1] = 1.5; // It was 1 before. - profile.counters = [ - getCounterForThreadWithSamples( - thread, - threadIndex, - { - time: sampleTimes.slice(), - // CPU usage numbers for the per-process CPU. - count: [100, 400, 500, 1000, 200, 500, 300, 100], - length: SAMPLE_COUNT, - }, - 'processCPU', - 'CPU' - ), - ]; + const counter = getCounterForThreadWithSamples( + thread, + threadIndex, + { + time: sampleTimes.slice(), + // CPU usage numbers for the per-process CPU. + count: [100, 400, 500, 1000, 200, 500, 300, 100], + length: SAMPLE_COUNT, + }, + 'processCPU', + 'CPU' + ); + counter.display = { + ...counter.display, + graphType: 'line-rate', + unit: 'percent', + sortWeight: 70, + label: 'Process CPU', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); const renderResult = render( - + ); const { container } = renderResult; @@ -84,13 +90,13 @@ describe('TrackProcessCPU', function () { flushRafCalls(); const canvas = ensureExists( - container.querySelector('.timelineTrackProcessCPUCanvas'), - `Couldn't find the process CPU canvas, with selector .timelineTrackProcessCPUCanvas` + container.querySelector('.timelineTrackCounterCanvas'), + `Couldn't find the process CPU canvas, with selector .timelineTrackCounterCanvas` ); const getTooltipContents = () => - document.querySelector('.timelineTrackProcessCPUTooltip'); + document.querySelector('.timelineTrackCounterTooltip'); const getProcessCPUDot = () => - container.querySelector('.timelineTrackProcessCPUGraphDot'); + container.querySelector('.timelineTrackCounterGraphDot'); const moveMouseAtCounter = (index: number, pos: number) => fireEvent( canvas, diff --git a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap index f1c568e5db..1e2bddd3a9 100644 --- a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap @@ -27,7 +27,7 @@ exports[`timeline/LocalTrack with a memory track matches the snapshot of the mem class="timelineTrackTrack" >
diff --git a/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap b/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap index 26028f6e40..a5e876b61a 100644 --- a/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackBandwidth.test.tsx.snap @@ -2,7 +2,7 @@ exports[`TrackBandwidth draws a dot that matches the snapshot 1`] = `
`; @@ -43,22 +43,22 @@ Array [ Array [ "lineTo", 15, - 23.877333333333333, + 24, ], Array [ "lineTo", 20, - 23.54, + 24, ], Array [ "lineTo", 30, - 23.54, + 24, ], Array [ "lineTo", 40, - 14.799999999999999, + 15, ], Array [ "lineTo", @@ -68,37 +68,37 @@ Array [ Array [ "lineTo", 60, - 23.862, + 24, ], Array [ "lineTo", 71, - 19.700934579439256, + 20, ], Array [ "lineTo", 71, - 23.99954, + 24, ], Array [ "lineTo", 71, - 14.799999999999804, + 15, ], Array [ "lineTo", 71, - 18.479999999999883, + 18, ], Array [ "lineTo", 110, - 23.882051282051282, + 24, ], Array [ "lineTo", 120, - 23.882051282051282, + 24, ], Array [ "stroke", @@ -121,15 +121,15 @@ Array [ exports[`TrackBandwidth matches the component snapshot 1`] = `
diff --git a/src/test/components/__snapshots__/TrackMemory.test.tsx.snap b/src/test/components/__snapshots__/TrackMemory.test.tsx.snap index 60376ab22d..eadc991dd5 100644 --- a/src/test/components/__snapshots__/TrackMemory.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackMemory.test.tsx.snap @@ -2,40 +2,40 @@ exports[`TrackMemory draws a dot that matches the snapshot 1`] = `
`; exports[`TrackMemory has a tooltip that matches the snapshot 1`] = `
0B relative memory at this time
2B memory range in graph
36 @@ -46,23 +46,23 @@ exports[`TrackMemory has a tooltip that matches the snapshot 1`] = ` exports[`TrackMemory has a tooltip that matches the snapshot for counts equalling zero 1`] = `
0B relative memory at this time
2B @@ -182,7 +182,7 @@ Array [ exports[`TrackMemory matches the component snapshot 1`] = `
diff --git a/src/test/components/__snapshots__/TrackPower.test.tsx.snap b/src/test/components/__snapshots__/TrackPower.test.tsx.snap index 7d6bb71bb8..5a4c451f8f 100644 --- a/src/test/components/__snapshots__/TrackPower.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackPower.test.tsx.snap @@ -2,7 +2,7 @@ exports[`TrackPower draws a dot that matches the snapshot 1`] = `
`; @@ -146,15 +146,15 @@ Array [ exports[`TrackPower matches the component snapshot 1`] = `
diff --git a/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap b/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap index c5adef80e4..1b29ea28cc 100644 --- a/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackProcessCPU.test.tsx.snap @@ -2,22 +2,22 @@ exports[`TrackProcessCPU draws a dot that matches the snapshot 1`] = `
`; exports[`TrackProcessCPU has a tooltip that matches the snapshot 1`] = `
CPU: 50% @@ -56,12 +56,12 @@ Array [ Array [ "moveTo", 0, - 24, + 26, ], Array [ "lineTo", 15, - 17.866666666666667, + 18, ], Array [ "lineTo", @@ -76,27 +76,27 @@ Array [ Array [ "lineTo", 40, - 19.4, + 19, ], Array [ "lineTo", 50, - 12.5, + 13, ], Array [ "lineTo", 60, - 17.1, + 17, ], Array [ "lineTo", 70, - 21.7, + 22, ], Array [ "lineTo", 80, - 21.7, + 22, ], Array [ "stroke", @@ -119,17 +119,19 @@ Array [ exports[`TrackProcessCPU matches the component snapshot 1`] = `
- +
+ +
diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 428b559836..59d2016366 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -1488,7 +1488,7 @@ const DEFAULT_TEST_COUNTER_DISPLAY: CounterDisplayConfig = { unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 50, + sortWeight: 35, label: 'My Counter', }; diff --git a/src/test/fixtures/profiles/tracks.ts b/src/test/fixtures/profiles/tracks.ts index ae23c71340..88df189e41 100644 --- a/src/test/fixtures/profiles/tracks.ts +++ b/src/test/fixtures/profiles/tracks.ts @@ -88,22 +88,11 @@ export function getHumanReadableTracks(state: State): string[] { for (const trackIndex of trackOrder) { const track = tracks[trackIndex]; let trackName; - if (track.type === 'memory') { - trackName = profileViewSelectors + if (track.type === 'counter') { + const counter = profileViewSelectors .getCounterSelectors(track.counterIndex) - .getPid(state); - } else if (track.type === 'bandwidth') { - trackName = profileViewSelectors - .getCounterSelectors(track.counterIndex) - .getPid(state); - } else if (track.type === 'process-cpu') { - trackName = profileViewSelectors - .getCounterSelectors(track.counterIndex) - .getPid(state); - } else if (track.type === 'power') { - trackName = profileViewSelectors - .getCounterSelectors(track.counterIndex) - .getCounter(state).name; + .getCounter(state); + trackName = counter.display.label || counter.name; } else if (track.type === 'marker') { trackName = stringArray[track.markerName]; } else { @@ -297,6 +286,15 @@ export function getStoreWithMemoryTrack(pid: Pid = '222') { thread.pid = pid; const counter = getCounterForThread(thread, threadIndex); counter.category = 'Memory'; + counter.display = { + ...counter.display, + graphType: 'line-accumulated', + unit: 'bytes', + color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; profile.counters = [counter]; } @@ -306,8 +304,8 @@ export function getStoreWithMemoryTrack(pid: Pid = '222') { trackReference ); - if (localTrack.type !== 'memory') { - throw new Error('Expected a memory track.'); + if (localTrack.type !== 'counter') { + throw new Error('Expected a counter track.'); } return { store, ...store, profile, trackReference, localTrack, threadIndex }; } diff --git a/src/test/store/profile-view.test.ts b/src/test/store/profile-view.test.ts index c78779dcf3..4bac18e1a6 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -511,8 +511,8 @@ describe('actions/ProfileView', function () { store.getState(), memoryTrackReference ); - if (memoryTrack.type !== 'memory') { - throw new Error('Expected to get memory track.'); + if (memoryTrack.type !== 'counter') { + throw new Error('Expected to get counter track.'); } } diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index e1a3802aa6..37172fd451 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -636,12 +636,9 @@ export type GlobalTrack = export type LocalTrack = | { readonly type: 'thread'; readonly threadIndex: ThreadIndex } | { readonly type: 'network'; readonly threadIndex: ThreadIndex } - | { readonly type: 'memory'; readonly counterIndex: CounterIndex } - | { readonly type: 'bandwidth'; readonly counterIndex: CounterIndex } + | { readonly type: 'counter'; readonly counterIndex: CounterIndex } | { readonly type: 'ipc'; readonly threadIndex: ThreadIndex } | { readonly type: 'event-delay'; readonly threadIndex: ThreadIndex } - | { readonly type: 'process-cpu'; readonly counterIndex: CounterIndex } - | { readonly type: 'power'; readonly counterIndex: CounterIndex } | { readonly type: 'marker'; readonly threadIndex: ThreadIndex; From 56a83555be1e4850bd7f127ed6b4c04b34601fef Mon Sep 17 00:00:00 2001 From: fatadel Date: Wed, 15 Apr 2026 15:26:10 +0200 Subject: [PATCH 3/8] Use counter.category/name for tooltip selection, remove redundant display prop --- src/components/timeline/TrackCounterGraph.tsx | 49 +++++++++++-------- src/test/components/TrackBandwidth.test.tsx | 2 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/timeline/TrackCounterGraph.tsx b/src/components/timeline/TrackCounterGraph.tsx index 0f70a5c7c8..a0f00ee300 100644 --- a/src/components/timeline/TrackCounterGraph.tsx +++ b/src/components/timeline/TrackCounterGraph.tsx @@ -47,7 +47,6 @@ import type { CssPixels, StartEndRange, IndexIntoSamplesTable, - CounterDisplayConfig, } from 'firefox-profiler/types'; import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; @@ -69,7 +68,6 @@ type CanvasProps = { readonly width: CssPixels; readonly height: CssPixels; readonly lineWidth: CssPixels; - readonly display: CounterDisplayConfig; }; /** @@ -97,8 +95,8 @@ class TrackCounterCanvas extends React.PureComponent { accumulatedSamples, maxCounterSampleCountPerMs, counterSampleRange, - display, } = this.props; + const { display } = counter; if (width === 0) { // Attempt to draw before the canvas was laid out. return; @@ -372,7 +370,6 @@ type StateProps = { readonly filteredThread: Thread; readonly unfilteredSamplesRange: StartEndRange | null; readonly previewSelection: PreviewSelection | null; - readonly display: CounterDisplayConfig; }; type DispatchProps = {}; @@ -387,8 +384,9 @@ 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 handles all counter types by - * branching on `display.graphType` for drawing and `display.unit` for tooltips. + * (eg, Memory, Power, etc.) as a graph in the timeline. It branches on + * `display.graphType` for drawing, and on `counter.category`/`counter.name` + * for tooltip rendering of known counter types. */ class TrackCounterGraphImpl extends React.PureComponent { override state = { @@ -458,7 +456,7 @@ class TrackCounterGraphImpl extends React.PureComponent { // For rate-based graphs with decimation, find the sample with the // highest value at the same pixel position. - if (this.props.display.graphType === 'line-rate') { + if (this.props.counter.display.graphType === 'line-rate') { const mouseAtTime = (t: number) => Math.round(((t - rangeStart) / rangeLength) * width + left); for ( @@ -535,10 +533,10 @@ class TrackCounterGraphImpl extends React.PureComponent { rangeStart, rangeEnd, interval, - display, maxCounterSampleCountPerMs, previewSelection, } = this.props; + const { display } = counter; const { mouseX, mouseY } = this.state; const { samples } = counter; @@ -555,10 +553,10 @@ class TrackCounterGraphImpl extends React.PureComponent { return null; } - const { unit } = display; + const { category, name } = counter; // Power tooltip — delegate to the dedicated component. - if (unit === 'pWh') { + if (category === 'power') { return ( { } // Process CPU tooltip. - if (unit === 'percent') { + if (category === 'CPU' && name === 'processCPU') { const cpuUsage = samples.count[counterIndex]; const sampleTimeDeltaInMs = counterIndex === 0 @@ -593,7 +591,7 @@ class TrackCounterGraphImpl extends React.PureComponent { } // Bandwidth tooltip — bytes with rate, CO2, and accumulated total. - if (unit === 'bytes' && display.graphType === 'line-rate') { + if (category === 'Bandwidth') { const { minCount, countRange, accumulatedCounts } = accumulatedSamples; const bytes = accumulatedCounts[counterIndex] - minCount; const operations = @@ -666,7 +664,7 @@ class TrackCounterGraphImpl extends React.PureComponent { } // Memory tooltip — accumulated bytes with operations count. - if (unit === 'bytes' && display.graphType === 'line-accumulated') { + if (category === 'Memory') { const { minCount, countRange, accumulatedCounts } = accumulatedSamples; const bytes = accumulatedCounts[counterIndex] - minCount; const operations = @@ -706,15 +704,28 @@ class TrackCounterGraphImpl extends React.PureComponent { ); } - // Default tooltip for unknown units — show the raw value. + // 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 (
- {formatNumber(samples.count[counterIndex])} + {formattedValue} - {counter.name} + {display.label || name}
@@ -736,10 +747,9 @@ class TrackCounterGraphImpl extends React.PureComponent { accumulatedSamples, maxCounterSampleCountPerMs, interval, - display, } = this.props; - const { samples } = counter; + const { samples, display } = counter; if (samples.length === 0) { throw new Error('No sample found for counter'); } @@ -804,7 +814,6 @@ class TrackCounterGraphImpl extends React.PureComponent { lineWidth, accumulatedSamples, maxCounterSampleCountPerMs, - display, } = this.props; return ( @@ -824,7 +833,6 @@ class TrackCounterGraphImpl extends React.PureComponent { interval={interval} accumulatedSamples={accumulatedSamples} maxCounterSampleCountPerMs={maxCounterSampleCountPerMs} - display={display} /> {hoveredCounter === null ? null : ( <> @@ -870,7 +878,6 @@ export const TrackCounterGraph = explicitConnect< filteredThread: selectors.getFilteredThread(state), unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), previewSelection: getPreviewSelection(state), - display: counter.display, }; }, component: withSize(TrackCounterGraphImpl), diff --git a/src/test/components/TrackBandwidth.test.tsx b/src/test/components/TrackBandwidth.test.tsx index 84de06ef60..f019d06bb5 100644 --- a/src/test/components/TrackBandwidth.test.tsx +++ b/src/test/components/TrackBandwidth.test.tsx @@ -79,7 +79,7 @@ describe('TrackBandwidth', function () { length: SAMPLE_COUNT, }, 'SystemBandwidth', - 'bandwidth' + 'Bandwidth' ); counter.display = { ...counter.display, From b3391ba3e048f8810b556ac2253b3a3b23ba8dfc Mon Sep 17 00:00:00 2001 From: fatadel Date: Tue, 21 Apr 2026 13:55:53 +0200 Subject: [PATCH 4/8] Give counter tracks a dedicated slot in LOCAL_TRACK_INDEX_ORDER Reshape LOCAL_TRACK_INDEX_ORDER and LOCAL_TRACK_DISPLAY_ORDER so that 'counter' occupies a real slot alongside the other local-track types. LOCAL_TRACK_DISPLAY_ORDER becomes the single source of truth for cross-type ordering; display.sortWeight is reduced to a tie-breaker between two counters. Add a v16 URL upgrader that remaps localTrackOrderByPid and hiddenLocalTracksByPid from the old per-counter-type indexes (memory=2, power=6, process-cpu=5, bandwidth=8) to the new single counter slot so existing URLs keep pointing at the same tracks. --- src/app-logic/url-handling.ts | 259 +++++++++++++++++- src/profile-logic/process-profile.ts | 24 +- src/profile-logic/tracks.ts | 109 +++----- .../fixtures/profiles/processed-profile.ts | 2 +- src/test/url-handling.test.ts | 137 +++++++++ 5 files changed, 444 insertions(+), 87 deletions(-) diff --git a/src/app-logic/url-handling.ts b/src/app-logic/url-handling.ts index eecd06a2be..0c7adeb457 100644 --- a/src/app-logic/url-handling.ts +++ b/src/app-logic/url-handling.ts @@ -53,7 +53,7 @@ import { StringTable } from 'firefox-profiler/utils/string-table'; import type { ProfileUpgradeInfo } from 'firefox-profiler/profile-logic/processed-profile-versioning'; import type { ProfileAndProfileUpgradeInfo } from 'firefox-profiler/actions/receive-profile'; -export const CURRENT_URL_VERSION = 15; +export const CURRENT_URL_VERSION = 16; /** * This static piece of state might look like an anti-pattern, but it's a relatively @@ -1351,8 +1351,265 @@ const _upgraders: { .map(mapIndexesInTransform) .join('~'); }, + [16]: ( + processedLocation: ProcessedLocationBeforeUpgrade, + profile?: Profile + ) => { + // The 'memory', 'power', 'bandwidth', and 'process-cpu' LocalTrack types + // have been collapsed into a single 'counter' type. This moves counter + // tracks into a single group inside each PID's local track array, which + // shifts the track indexes that older URLs recorded. Remap + // localTrackOrderByPid and hiddenLocalTracksByPid so existing URLs keep + // pointing at the same tracks. + const { query } = processedLocation; + if (!profile || !profile.counters || profile.counters.length === 0) { + return; + } + if (!query.localTrackOrderByPid && !query.hiddenLocalTracksByPid) { + return; + } + + const oldToNewIndexByPid = _computeV16LocalTrackIndexRemap(profile); + + if (query.localTrackOrderByPid) { + query.localTrackOrderByPid = (query.localTrackOrderByPid as string) + .split('~') + .map((pidAndTracks) => { + const dash = pidAndTracks.indexOf('-'); + if (dash === -1) { + return pidAndTracks; + } + const pid = pidAndTracks.slice(0, dash); + const encoded = pidAndTracks.slice(dash + 1); + const remap = oldToNewIndexByPid.get(pid); + if (!remap) { + return pidAndTracks; + } + const oldOrder = decodeUintArrayFromUrlComponent(encoded); + const newOrder = []; + for (const oldIndex of oldOrder) { + const newIndex = remap[oldIndex]; + if (newIndex !== undefined && newIndex !== null) { + newOrder.push(newIndex); + } + } + return `${pid}-${encodeUintArrayForUrlComponent(newOrder)}`; + }) + .join('~'); + } + + if (query.hiddenLocalTracksByPid) { + query.hiddenLocalTracksByPid = (query.hiddenLocalTracksByPid as string) + .split('~') + .map((pidAndTracks) => { + const dash = pidAndTracks.indexOf('-'); + if (dash === -1) { + return pidAndTracks; + } + const pid = pidAndTracks.slice(0, dash); + const encoded = pidAndTracks.slice(dash + 1); + const remap = oldToNewIndexByPid.get(pid); + if (!remap) { + return pidAndTracks; + } + const oldHidden = decodeUintArrayFromUrlComponent(encoded); + const newHidden = new Set(); + for (const oldIndex of oldHidden) { + const newIndex = remap[oldIndex]; + if (newIndex !== undefined && newIndex !== null) { + newHidden.add(newIndex); + } + } + return `${pid}-${encodeUintSetForUrlComponent(newHidden)}`; + }) + .join('~'); + } + }, }; +/** + * Produce a per-PID mapping from old local-track indexes (using the pre-v16 + * LOCAL_TRACK_INDEX_ORDER) to new local-track indexes (post-v16). Used by the + * v16 URL upgrader. + * + * The two layouts share the same set of tracks; only the relative positions of + * counter tracks differ. The helper reconstructs both layouts by simulating + * what computeLocalTracksByPid would have produced under each. + */ +function _computeV16LocalTrackIndexRemap( + profile: Profile +): Map> { + // Pre-v16 (main) LOCAL_TRACK_INDEX_ORDER. Frozen snapshot — do not change. + const OLD_SLOT = { + thread: 0, + network: 1, + memory: 2, + ipc: 3, + marker: 7, + power: 6, + bandwidth: 8, + }; + // Post-v16 LOCAL_TRACK_INDEX_ORDER. Frozen snapshot — do not change even if + // production LOCAL_TRACK_INDEX_ORDER evolves later. + const NEW_SLOT = { + thread: 0, + network: 1, + counter: 2, + ipc: 3, + marker: 5, + }; + + type Entry = { + id: string; + oldSlot: number | null; + newSlot: number | null; + }; + + const entriesByPid = new Map(); + const ensurePid = (pid: Pid): Entry[] => { + let entries = entriesByPid.get(pid); + if (entries === undefined) { + entries = []; + entriesByPid.set(pid, entries); + } + return entries; + }; + + const markerSchemasWithGraphs = (profile.meta.markerSchema || []).filter( + (schema) => Array.isArray(schema.graphs) && schema.graphs.length > 0 + ); + + for ( + let threadIndex = 0; + threadIndex < profile.threads.length; + threadIndex++ + ) { + const thread = profile.threads[threadIndex]; + const { pid, markers } = thread; + + if (!thread.isMainThread) { + ensurePid(pid).push({ + id: `t:${threadIndex}`, + oldSlot: OLD_SLOT.thread, + newSlot: NEW_SLOT.thread, + }); + } + if (markers.data.some((datum) => datum && datum.type === 'Network')) { + ensurePid(pid).push({ + id: `n:${threadIndex}`, + oldSlot: OLD_SLOT.network, + newSlot: NEW_SLOT.network, + }); + } + if (markers.data.some((datum) => datum && datum.type === 'IPC')) { + ensurePid(pid).push({ + id: `i:${threadIndex}`, + oldSlot: OLD_SLOT.ipc, + newSlot: NEW_SLOT.ipc, + }); + } + + if (markerSchemasWithGraphs.length > 0) { + const markerTracksBySchemaName: Map< + string, + { keys: string[]; markerNames: Set } + > = new Map(); + for (const markerSchema of markerSchemasWithGraphs) { + markerTracksBySchemaName.set(markerSchema.name, { + keys: (markerSchema.graphs || []).map((graph) => graph.key), + markerNames: new Set(), + }); + } + for (let i = 0; i < markers.length; ++i) { + const markerNameIndex = markers.name[i]; + const markerData = markers.data[i]; + if (markerData && markerData.type) { + const mapEntry = markerTracksBySchemaName.get(markerData.type); + if (mapEntry && mapEntry.keys.every((k) => k in markerData)) { + mapEntry.markerNames.add(markerNameIndex); + } + } + } + for (const [schemaName, { markerNames }] of markerTracksBySchemaName) { + for (const markerName of markerNames) { + ensurePid(pid).push({ + id: `m:${threadIndex}:${schemaName}:${markerName}`, + oldSlot: OLD_SLOT.marker, + newSlot: NEW_SLOT.marker, + }); + } + } + } + } + + const { counters } = profile; + if (counters) { + for (let counterIndex = 0; counterIndex < counters.length; counterIndex++) { + const counter = counters[counterIndex]; + const { pid, category, name, samples } = counter; + + // OLD behavior: only Memory / Bandwidth / Power produced tracks. + let oldSlot: number | null; + if (category === 'Memory') { + oldSlot = OLD_SLOT.memory; + } else if (category === 'Bandwidth') { + oldSlot = OLD_SLOT.bandwidth; + } + // We assume there is no data when <= 2 samples + else if (category === 'power' && samples.length > 2) { + oldSlot = OLD_SLOT.power; + } else { + oldSlot = null; + } + + // NEW behavior: mirror computeLocalTracksByPid. processCPU counters are + // added later by addProcessCPUTracksForProcess when the experimental + // toggle fires, every other counter becomes a track. + let newSlot: number | null; + if (category === 'CPU' && name === 'processCPU') { + newSlot = null; + } else if (category === 'power' && samples.length <= 2) { + newSlot = null; + } else { + newSlot = NEW_SLOT.counter; + } + + if (oldSlot === null && newSlot === null) { + continue; + } + + ensurePid(pid).push({ + id: `c:${counterIndex}`, + oldSlot, + newSlot, + }); + } + } + + const remapByPid = new Map>(); + for (const [pid, entries] of entriesByPid) { + const oldList = entries + .filter((e) => e.oldSlot !== null) + .slice() + .sort((a, b) => (a.oldSlot as number) - (b.oldSlot as number)); + const newList = entries + .filter((e) => e.newSlot !== null) + .slice() + .sort((a, b) => (a.newSlot as number) - (b.newSlot as number)); + + const newIdToIndex = new Map(); + newList.forEach((entry, i) => newIdToIndex.set(entry.id, i)); + + const remap: Array = oldList.map((entry) => { + const newIndex = newIdToIndex.get(entry.id); + return newIndex === undefined ? null : newIndex; + }); + remapByPid.set(pid, remap); + } + + return remapByPid; +} + for (let destVersion = 1; destVersion <= CURRENT_URL_VERSION; destVersion++) { if (!_upgraders[destVersion]) { throw new Error(`There is no upgrader for version ${destVersion}.`); diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index c760201465..709b3c048e 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -980,7 +980,16 @@ function _deriveCounterDisplay( category: string, name: string ): CounterDisplayConfig { - if (category === 'Memory') { + if (category === 'Bandwidth') { + return { + graphType: 'line-rate', + unit: 'bytes', + color: 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'Memory') { return { graphType: 'line-accumulated', unit: 'bytes', @@ -998,22 +1007,13 @@ function _deriveCounterDisplay( sortWeight: 30, label: name, }; - } else if (category === 'Bandwidth') { - return { - graphType: 'line-rate', - unit: 'bytes', - color: 'blue', - markerSchemaLocation: null, - sortWeight: 10, - label: 'Bandwidth', - }; } else if (category === 'CPU' && name === 'processCPU') { return { graphType: 'line-rate', unit: 'percent', color: 'grey', markerSchemaLocation: null, - sortWeight: 70, + sortWeight: 40, label: 'Process CPU', }; } @@ -1023,7 +1023,7 @@ function _deriveCounterDisplay( unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 35, + sortWeight: 50, label: name, }; } diff --git a/src/profile-logic/tracks.ts b/src/profile-logic/tracks.ts index 4e61ddf608..0d8ce5c393 100644 --- a/src/profile-logic/tracks.ts +++ b/src/profile-logic/tracks.ts @@ -56,49 +56,24 @@ export type HiddenTracks = { const LOCAL_TRACK_INDEX_ORDER = { thread: 0, network: 1, - // Needed only for ts types; counter tracks use _getCounterTrackIndexOrder(). - counter: -1, + counter: 2, ipc: 3, 'event-delay': 4, - marker: 7, + marker: 5, }; const LOCAL_TRACK_DISPLAY_ORDER = { network: 0, - // Needed only for ts types; counter tracks use display.sortWeight. - counter: -1, + counter: 1, // IPC tracks that belong to the global track will appear right after network // and counter tracks. But we want to show the IPC tracks that belong to the // local threads right after their track. This special handling happens inside // the sort function. - ipc: 40, - thread: 50, - 'event-delay': 60, - marker: 80, + ipc: 2, + thread: 3, + 'event-delay': 4, + marker: 5, }; -/** - * Map a counter's category and name to the old LOCAL_TRACK_INDEX_ORDER value - * that was used before all counter types were unified. This is needed for - * backward compatibility of URL-encoded track indexes. - */ -function _getCounterTrackIndexOrder(counter: RawCounter): number { - const { category, name } = counter; - if (category === 'Memory') { - return 2; - } - if (category === 'CPU' && name === 'processCPU') { - return 5; - } - if (category === 'power') { - return 6; - } - if (category === 'Bandwidth') { - return 8; - } - // Unknown counter types go after all known types. - return 9; -} - const GLOBAL_TRACK_INDEX_ORDER = { process: 0, screenshots: 1, @@ -122,6 +97,27 @@ function _getDefaultLocalTrackOrder( const naturalSort = new Intl.Collator('en-US', { numeric: true }); // In place sort! trackOrder.sort((a, b) => { + // Tie-break between two counter tracks using their display.sortWeight. + // Cross-type ordering is handled below by LOCAL_TRACK_DISPLAY_ORDER. + if ( + tracks[a].type === 'counter' && + tracks[b].type === 'counter' && + profile && + profile.counters + ) { + const counterA = profile.counters[tracks[a].counterIndex]; + const counterB = profile.counters[tracks[b].counterIndex]; + const sortWeightDiff = + counterA.display.sortWeight - counterB.display.sortWeight; + if (sortWeightDiff !== 0) { + return sortWeightDiff; + } + if (profile.meta.keepProfileThreadOrder) { + return tracks[a].counterIndex - tracks[b].counterIndex; + } + return naturalSort.compare(counterA.name, counterB.name); + } + if ( tracks[a].type === 'thread' && tracks[b].type === 'ipc' && @@ -142,27 +138,6 @@ function _getDefaultLocalTrackOrder( return 1; } - if ( - profile && - profile.counters && - tracks[a].type === 'counter' && - tracks[b].type === 'counter' - ) { - const counterA = profile.counters[tracks[a].counterIndex]; - const counterB = profile.counters[tracks[b].counterIndex]; - // Sort counter tracks by their display.sortWeight first. - const sortWeightDiff = - counterA.display.sortWeight - counterB.display.sortWeight; - if (sortWeightDiff !== 0) { - return sortWeightDiff; - } - // Within the same sortWeight, sort by name. - if (profile.meta.keepProfileThreadOrder) { - return tracks[a].counterIndex - tracks[b].counterIndex; - } - return naturalSort.compare(counterA.name, counterB.name); - } - // If the tracks are both threads, sort them by thread name, and then by // creation time if they have the same name. if (tracks[a].type === 'thread' && tracks[b].type === 'thread' && profile) { @@ -182,15 +157,10 @@ function _getDefaultLocalTrackOrder( ); } - const displayA = - tracks[a].type === 'counter' && profile && profile.counters - ? profile.counters[tracks[a].counterIndex].display.sortWeight - : LOCAL_TRACK_DISPLAY_ORDER[tracks[a].type]; - const displayB = - tracks[b].type === 'counter' && profile && profile.counters - ? profile.counters[tracks[b].counterIndex].display.sortWeight - : LOCAL_TRACK_DISPLAY_ORDER[tracks[b].type]; - return displayA - displayB; + return ( + LOCAL_TRACK_DISPLAY_ORDER[tracks[a].type] - + LOCAL_TRACK_DISPLAY_ORDER[tracks[b].type] + ); }); return trackOrder; @@ -459,17 +429,10 @@ export function computeLocalTracksByPid( // added at the end so that the local track indexes are stable and backwards compatible. for (const localTracks of localTracksByPid.values()) { // In place sort! - localTracks.sort((a: LocalTrack, b: LocalTrack) => { - const orderA = - a.type === 'counter' && counters - ? _getCounterTrackIndexOrder(counters[a.counterIndex]) - : LOCAL_TRACK_INDEX_ORDER[a.type]; - const orderB = - b.type === 'counter' && counters - ? _getCounterTrackIndexOrder(counters[b.counterIndex]) - : LOCAL_TRACK_INDEX_ORDER[b.type]; - return orderA - orderB; - }); + localTracks.sort( + (a: LocalTrack, b: LocalTrack) => + LOCAL_TRACK_INDEX_ORDER[a.type] - LOCAL_TRACK_INDEX_ORDER[b.type] + ); } return localTracksByPid; diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 59d2016366..428b559836 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -1488,7 +1488,7 @@ const DEFAULT_TEST_COUNTER_DISPLAY: CounterDisplayConfig = { unit: '', color: 'grey', markerSchemaLocation: null, - sortWeight: 35, + sortWeight: 50, label: 'My Counter', }; diff --git a/src/test/url-handling.test.ts b/src/test/url-handling.test.ts index 88ff994ee0..cbd4c28c95 100644 --- a/src/test/url-handling.test.ts +++ b/src/test/url-handling.test.ts @@ -51,6 +51,7 @@ import { import { getProfileFromTextSamples, getProfileWithMarkers, + getCounterForThread, } from './fixtures/profiles/processed-profile'; import { selectedThreadSelectors } from '../selectors/per-thread'; import { @@ -1278,6 +1279,142 @@ describe('url upgrading', function () { }); }); + describe('version 16: collapse counter track types into a single counter type', function () { + // Build a profile whose pid '222' has one main thread with an IPC marker and + // three counters (Memory, power, Bandwidth). This exercises the v16 upgrader: + // under the pre-v16 LOCAL_TRACK_INDEX_ORDER the ipc track sat between + // 'memory' (slot 2) and 'power' (slot 6), which no longer holds now that all + // counters share slot 2. + function buildCounterProfile() { + const { profile } = getProfileFromTextSamples('A B C D E'); + const mainThread = profile.threads[0]; + mainThread.pid = '222'; + mainThread.name = 'GeckoMain'; + mainThread.isMainThread = true; + mainThread.processType = 'tab'; + + // Push a single IPC marker onto the main thread. The upgrader only + // checks markers.data[i].type === 'IPC', so a minimal payload is enough. + const stringTable = StringTable.withBackingArray( + profile.shared.stringArray + ); + mainThread.markers.name.push(stringTable.indexForString('IPC')); + mainThread.markers.phase.push(0); + mainThread.markers.startTime.push(0); + mainThread.markers.endTime.push(null); + mainThread.markers.category.push(0); + mainThread.markers.data.push({ + type: 'IPC', + startTime: 0, + endTime: 1, + otherPid: '333', + messageSeqno: 1, + messageType: 'Foo', + side: 'parent', + direction: 'sending', + phase: 'endpoint', + sync: false, + } as any); + mainThread.markers.length++; + + const memoryCounter = getCounterForThread(mainThread, 0); + memoryCounter.category = 'Memory'; + memoryCounter.name = 'Memory'; + const powerCounter = getCounterForThread(mainThread, 0); + powerCounter.category = 'power'; + powerCounter.name = 'Power'; + const bandwidthCounter = getCounterForThread(mainThread, 0); + bandwidthCounter.category = 'Bandwidth'; + bandwidthCounter.name = 'Bandwidth'; + profile.counters = [memoryCounter, powerCounter, bandwidthCounter]; + + return profile; + } + + it('remaps localTrackOrderByPid when counter tracks change position', function () { + const profile = buildCounterProfile(); + + // Pre-v16 layout for pid '222' (sorted by old LOCAL_TRACK_INDEX_ORDER): + // [0] memory (slot 2) + // [1] ipc (slot 3) + // [2] power (slot 6) + // [3] bandwidth (slot 8) + const oldOrder = [3, 2, 1, 0]; + + // Post-v16 layout: counters grouped at slot 2, ipc at slot 3. + // [0] memory (counter, insertion order) + // [1] power (counter) + // [2] bandwidth (counter) + // [3] ipc + const expectedNewOrder = [2, 1, 3, 0]; + + const { query } = upgradeLocationToCurrentVersion( + { + pathname: '', + hash: '', + query: { + v: '15', + localTrackOrderByPid: + '222-' + encodeUintArrayForUrlComponent(oldOrder), + }, + }, + profile + ); + + expect(query.localTrackOrderByPid).toBe( + '222-' + encodeUintArrayForUrlComponent(expectedNewOrder) + ); + }); + + it('remaps hiddenLocalTracksByPid when counter tracks change position', function () { + const profile = buildCounterProfile(); + + // Hide the old 'ipc' (index 1) and old 'bandwidth' (index 3). + const oldHidden = new Set([1, 3]); + // In the new layout those tracks live at indexes 3 (ipc) and 2 (bandwidth). + const expectedNewHidden = new Set([3, 2]); + + const { query } = upgradeLocationToCurrentVersion( + { + pathname: '', + hash: '', + query: { + v: '15', + hiddenLocalTracksByPid: + '222-' + encodeUintSetForUrlComponent(oldHidden), + }, + }, + profile + ); + + expect(query.hiddenLocalTracksByPid).toBe( + '222-' + encodeUintSetForUrlComponent(expectedNewHidden) + ); + }); + + it('leaves unrelated PIDs untouched', function () { + const profile = buildCounterProfile(); + + // pid '999' isn't in the profile, so the upgrader has nothing to remap + // for it and should leave the segment as-is. + const untouched = '999-' + encodeUintArrayForUrlComponent([1, 0]); + + const { query } = upgradeLocationToCurrentVersion( + { + pathname: '', + hash: '', + query: { + v: '15', + localTrackOrderByPid: untouched, + }, + }, + profile + ); + + expect(query.localTrackOrderByPid).toBe(untouched); + }); + }); + // More general checks it("won't run if the current version is specified", function () { const { getState } = _getStoreWithURL({ From 53d42a2b34c2fa806d6a8c96c6a8ca4b62d5746f Mon Sep 17 00:00:00 2001 From: fatadel Date: Tue, 21 Apr 2026 17:11:22 +0200 Subject: [PATCH 5/8] Tighten TrackCounter wrapper and narrow graphType dispatch - Pass a boolean hasMarkers prop to TrackCounterGraph instead of the whole CounterDisplayConfig, and move the track wrapper's sizing into CSS so the TSX stays free of inline style. - Use a switch with assertExhaustiveCheck(display.graphType) in both the canvas draw and the hovered-sample dot, so adding a new graph type surfaces as a TypeScript error at those sites. - Replace the optional-chain + null-and-undefined check on counters with ensureExists, both in getTimelineHeight and in getProcessesWithMemoryTrack. - Drop a stray Math.round from the rate path's y-coordinate so rate counter snapshots match their pre-unification values. --- src/components/timeline/TrackCounter.css | 6 +- src/components/timeline/TrackCounter.tsx | 27 +- src/components/timeline/TrackCounterGraph.tsx | 299 +++++++++--------- src/selectors/app.tsx | 10 +- src/selectors/profile.ts | 4 +- .../__snapshots__/LocalTrack.test.tsx.snap | 1 - .../TrackBandwidth.test.tsx.snap | 23 +- .../__snapshots__/TrackMemory.test.tsx.snap | 1 - .../__snapshots__/TrackPower.test.tsx.snap | 23 +- .../TrackProcessCPU.test.tsx.snap | 13 +- 10 files changed, 199 insertions(+), 208 deletions(-) diff --git a/src/components/timeline/TrackCounter.css b/src/components/timeline/TrackCounter.css index c45e73fdf9..da8996786e 100644 --- a/src/components/timeline/TrackCounter.css +++ b/src/components/timeline/TrackCounter.css @@ -5,7 +5,7 @@ .timelineTrackCounterGraph { position: relative; width: 100%; - height: var(--graph-height); + height: 25px; } .timelineTrackCounterCanvas { @@ -35,7 +35,7 @@ font-weight: bold; } -.timelineTrackCounterMarkers { - height: var(--markers-height, 15px); +.timelineMarkersMemory { + height: 15px; opacity: 1; } diff --git a/src/components/timeline/TrackCounter.tsx b/src/components/timeline/TrackCounter.tsx index dfc7c005b2..bae0d74c49 100644 --- a/src/components/timeline/TrackCounter.tsx +++ b/src/components/timeline/TrackCounter.tsx @@ -13,7 +13,6 @@ import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; import { TrackCounterGraph } from './TrackCounterGraph'; import { TRACK_COUNTER_GRAPH_HEIGHT, - TRACK_COUNTER_MARKERS_HEIGHT, TRACK_COUNTER_LINE_WIDTH, } from 'firefox-profiler/app-logic/constants'; @@ -21,7 +20,6 @@ import type { CounterIndex, ThreadIndex, Milliseconds, - CounterDisplayConfig, } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; @@ -36,7 +34,7 @@ type StateProps = { readonly threadIndex: ThreadIndex; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; - readonly display: CounterDisplayConfig; + readonly hasMarkers: boolean; }; type DispatchProps = { @@ -56,26 +54,11 @@ export class TrackCounterImpl extends React.PureComponent { }; override render() { - const { counterIndex, rangeStart, rangeEnd, threadIndex, display } = + const { counterIndex, rangeStart, rangeEnd, threadIndex, hasMarkers } = this.props; - const hasMarkers = display.markerSchemaLocation !== null; - const graphHeight = TRACK_COUNTER_GRAPH_HEIGHT; - const totalHeight = hasMarkers - ? graphHeight + TRACK_COUNTER_MARKERS_HEIGHT - : graphHeight; - return ( -
+
{hasMarkers ? ( {
); @@ -108,7 +91,7 @@ export const TrackCounter = explicitConnect< threadIndex: counter.mainThreadIndex, rangeStart: start, rangeEnd: end, - display: counter.display, + hasMarkers: Boolean(counter.display.markerSchemaLocation), }; }, mapDispatchToProps: { updatePreviewSelection }, diff --git a/src/components/timeline/TrackCounterGraph.tsx b/src/components/timeline/TrackCounterGraph.tsx index a0f00ee300..16a3d3a501 100644 --- a/src/components/timeline/TrackCounterGraph.tsx +++ b/src/components/timeline/TrackCounterGraph.tsx @@ -34,6 +34,7 @@ import { } from 'firefox-profiler/components/tooltip/TooltipDetails'; import { EmptyThreadIndicator } from './EmptyThreadIndicator'; import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; +import { assertExhaustiveCheck } from 'firefox-profiler/utils/types'; import { co2 } from '@tgwf/co2'; import type { @@ -146,151 +147,160 @@ class TrackCounterCanvas extends React.PureComponent { ctx.fillStyle = getFillColor(display.color); ctx.beginPath(); - if (display.graphType === 'line-accumulated') { - // Accumulated graph: plot the running total. - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - - // The x and y are used after the loop. - let x = 0; - let y = 0; - let firstX = 0; - for (let i = sampleStart; i < sampleEnd; i++) { - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - x = (samples.time[i] - rangeStart) * millisecondWidth; - // Add on half the stroke's line width so that it won't be cut off the edge - // of the graph. - const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange; - y = - innerDeviceHeight - - innerDeviceHeight * unitGraphCount + - deviceLineHalfWidth; - if (i === sampleStart) { - // This is the first iteration, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - firstX = x; - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); + switch (display.graphType) { + case 'line-accumulated': { + // Accumulated graph: plot the running total. + const { minCount, countRange, accumulatedCounts } = + accumulatedSamples; + + // The x and y are used after the loop. + let x = 0; + let y = 0; + let firstX = 0; + for (let i = sampleStart; i < sampleEnd; i++) { + // Create a path for the top of the chart. This is the line that will have + // a stroke applied to it. + x = (samples.time[i] - rangeStart) * millisecondWidth; + // Add on half the stroke's line width so that it won't be cut off the edge + // of the graph. + const unitGraphCount = + (accumulatedCounts[i] - minCount) / countRange; + y = + innerDeviceHeight - + innerDeviceHeight * unitGraphCount + + deviceLineHalfWidth; + if (i === sampleStart) { + // This is the first iteration, only move the line, do not draw it. Also + // remember this first X, as the bottom of the graph will need to connect + // back up to it. + firstX = x; + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } } - } - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); + // The samples range ends at the time of the last sample, plus the interval. + // Draw this last bit. + ctx.lineTo(x + intervalWidth, y); - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); + // Don't do the fill yet, just stroke the top line. This will draw a line from + // point 1 to 2 in the diagram above. + ctx.stroke(); - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. + // After doing the stroke, continue the path to complete the fill to the bottom + // of the canvas. This continues the path to point 3 and then 4. - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); + // Create a line from 2 to 3. + ctx.lineTo(x + intervalWidth, deviceHeight); - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); + // Create a line from 3 to 4. + ctx.lineTo(firstX, deviceHeight); - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); - } else { - // Rate graph: plot count / timeDelta with min-max decimation. - const countRangePerMs = maxCounterSampleCountPerMs; - - const getX = (i: number) => - Math.round((samples.time[i] - rangeStart) * millisecondWidth); - const getY = (rawY: number) => { - if (!rawY) { - // Make the 0 values invisible so that 'almost 0' is noticeable. - return deviceHeight + deviceLineHalfWidth; - } - const unitGraphCount = rawY / countRangePerMs; - return Math.round( - innerDeviceHeight - + // The line from 4 to 1 will be implicitly filled in. + ctx.fill(); + break; + } + case 'line-rate': { + // Rate graph: plot count / timeDelta with min-max decimation. + const countRangePerMs = maxCounterSampleCountPerMs; + + const getX = (i: number) => + Math.round((samples.time[i] - rangeStart) * millisecondWidth); + const getY = (rawY: number) => { + if (!rawY) { + // Make the 0 values invisible so that 'almost 0' is noticeable. + return deviceHeight + deviceLineHalfWidth; + } + const unitGraphCount = rawY / countRangePerMs; + return ( + innerDeviceHeight - innerDeviceHeight * unitGraphCount + // Add on half the stroke's line width so that it won't be cut off the edge // of the graph. deviceLineHalfWidth - ); - }; - - const getRate = (i: number) => { - const sampleTimeDeltaInMs = - i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - return samples.count[i] / sampleTimeDeltaInMs; - }; - - // The x and y are used after the loop. - const firstX = getX(sampleStart); - let x = firstX; - let y = getY(getRate(sampleStart)); - - // For the first sample, only move the line, do not draw it. Also - // remember this first X, as the bottom of the graph will need to connect - // back up to it. - ctx.moveTo(x, y); - - // Create a path for the top of the chart. This is the line that will have - // a stroke applied to it. - for (let i = sampleStart + 1; i < sampleEnd; i++) { - const rateValues = [getRate(i)]; - x = getX(i); - y = getY(rateValues[0]); - ctx.lineTo(x, y); - - // If we have multiple samples to draw on the same horizontal pixel, - // we process all of them together with a max-min decimation algorithm - // to save time: - // - We draw the first and last samples to ensure the display is - // correct if there are sampling gaps. - // - For the values in between, we only draw the min and max values, - // to draw a vertical line covering all the other sample values. - while (i + 1 < sampleEnd && getX(i + 1) === x) { - rateValues.push(getRate(++i)); - } + ); + }; - // Looking for the min and max only makes sense if we have more than 2 - // samples to draw. - if (rateValues.length > 2) { - const minY = getY(Math.min(...rateValues)); - if (minY !== y) { - y = minY; - ctx.lineTo(x, y); + const getRate = (i: number) => { + const sampleTimeDeltaInMs = + i === 0 ? interval : samples.time[i] - samples.time[i - 1]; + return samples.count[i] / sampleTimeDeltaInMs; + }; + + // The x and y are used after the loop. + const firstX = getX(sampleStart); + let x = firstX; + let y = getY(getRate(sampleStart)); + + // For the first sample, only move the line, do not draw it. Also + // remember this first X, as the bottom of the graph will need to connect + // back up to it. + ctx.moveTo(x, y); + + // Create a path for the top of the chart. This is the line that will have + // a stroke applied to it. + for (let i = sampleStart + 1; i < sampleEnd; i++) { + const rateValues = [getRate(i)]; + x = getX(i); + y = getY(rateValues[0]); + ctx.lineTo(x, y); + + // If we have multiple samples to draw on the same horizontal pixel, + // we process all of them together with a max-min decimation algorithm + // to save time: + // - We draw the first and last samples to ensure the display is + // correct if there are sampling gaps. + // - For the values in between, we only draw the min and max values, + // to draw a vertical line covering all the other sample values. + while (i + 1 < sampleEnd && getX(i + 1) === x) { + rateValues.push(getRate(++i)); } - const maxY = getY(Math.max(...rateValues)); - if (maxY !== y) { - y = maxY; - ctx.lineTo(x, y); + + // Looking for the min and max only makes sense if we have more than 2 + // samples to draw. + if (rateValues.length > 2) { + const minY = getY(Math.min(...rateValues)); + if (minY !== y) { + y = minY; + ctx.lineTo(x, y); + } + const maxY = getY(Math.max(...rateValues)); + if (maxY !== y) { + y = maxY; + ctx.lineTo(x, y); + } } - } - const lastY = getY(rateValues[rateValues.length - 1]); - if (lastY !== y) { - y = lastY; - ctx.lineTo(x, y); + const lastY = getY(rateValues[rateValues.length - 1]); + if (lastY !== y) { + y = lastY; + ctx.lineTo(x, y); + } } - } - // The samples range ends at the time of the last sample, plus the interval. - // Draw this last bit. - ctx.lineTo(x + intervalWidth, y); + // The samples range ends at the time of the last sample, plus the interval. + // Draw this last bit. + ctx.lineTo(x + intervalWidth, y); - // Don't do the fill yet, just stroke the top line. This will draw a line from - // point 1 to 2 in the diagram above. - ctx.stroke(); + // Don't do the fill yet, just stroke the top line. This will draw a line from + // point 1 to 2 in the diagram above. + ctx.stroke(); - // After doing the stroke, continue the path to complete the fill to the bottom - // of the canvas. This continues the path to point 3 and then 4. + // After doing the stroke, continue the path to complete the fill to the bottom + // of the canvas. This continues the path to point 3 and then 4. - // Create a line from 2 to 3. - ctx.lineTo(x + intervalWidth, deviceHeight); + // Create a line from 2 to 3. + ctx.lineTo(x + intervalWidth, deviceHeight); - // Create a line from 3 to 4. - ctx.lineTo(firstX, deviceHeight); + // Create a line from 3 to 4. + ctx.lineTo(firstX, deviceHeight); - // The line from 4 to 1 will be implicitly filled in. - ctx.fill(); + // The line from 4 to 1 will be implicitly filled in. + ctx.fill(); + break; + } + default: + throw assertExhaustiveCheck(display.graphType); } } } @@ -768,23 +778,30 @@ class TrackCounterGraphImpl extends React.PureComponent { const innerTrackHeight = graphHeight - lineWidth / 2; let top; - if (display.graphType === 'line-accumulated') { - const { minCount, countRange, accumulatedCounts } = accumulatedSamples; - const unitSampleCount = - (accumulatedCounts[counterIndex] - minCount) / countRange; - top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; - } else { - const sampleTimeDeltaInMs = - counterIndex === 0 - ? interval - : samples.time[counterIndex] - samples.time[counterIndex - 1]; - const unitSampleCount = - samples.count[counterIndex] / - sampleTimeDeltaInMs / - maxCounterSampleCountPerMs; - top = - innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + switch (display.graphType) { + case 'line-accumulated': { + const { minCount, countRange, accumulatedCounts } = accumulatedSamples; + const unitSampleCount = + (accumulatedCounts[counterIndex] - minCount) / countRange; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + break; + } + case 'line-rate': { + const sampleTimeDeltaInMs = + counterIndex === 0 + ? interval + : samples.time[counterIndex] - samples.time[counterIndex - 1]; + const unitSampleCount = + samples.count[counterIndex] / + sampleTimeDeltaInMs / + maxCounterSampleCountPerMs; + top = + innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2; + break; + } + default: + throw assertExhaustiveCheck(display.graphType); } return ( diff --git a/src/selectors/app.tsx b/src/selectors/app.tsx index e978f606c2..c477985be5 100644 --- a/src/selectors/app.tsx +++ b/src/selectors/app.tsx @@ -186,12 +186,10 @@ export const getTimelineHeight: Selector = createSelector( height += TRACK_NETWORK_HEIGHT + border; break; case 'counter': { - // Counter track height depends on whether it has markers. - const counter = counters?.[localTrack.counterIndex]; - const hasMarkers = - counter?.display.markerSchemaLocation !== null && - counter?.display.markerSchemaLocation !== undefined; - height += hasMarkers + // Counter track height depends on whether the counter asks for + // markers to be rendered above the graph. + const counter = ensureExists(counters)[localTrack.counterIndex]; + height += counter.display.markerSchemaLocation ? TRACK_COUNTER_GRAPH_HEIGHT + TRACK_COUNTER_MARKERS_HEIGHT : TRACK_COUNTER_GRAPH_HEIGHT; height += border; diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 9501c18fda..c35e9d0050 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -557,9 +557,7 @@ export const getProcessesWithMemoryTrack: Selector> = createSelector( localTracks.some( (track) => track.type === 'counter' && - counters !== null && - counters[track.counterIndex].display.markerSchemaLocation === - 'timeline-memory' + ensureExists(counters)[track.counterIndex].category === 'Memory' ) ) { processesWithMemoryTrack.add(pid); diff --git a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap index 1e2bddd3a9..c6a9af08e0 100644 --- a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap @@ -28,7 +28,6 @@ exports[`timeline/LocalTrack with a memory track matches the snapshot of the mem >
Date: Thu, 23 Apr 2026 17:27:56 +0200 Subject: [PATCH 6/8] Drive the counter marker row from display.markerSchemaLocation Rework TrackCounter so its optional marker row filters by the counter's own display.markerSchemaLocation rather than reusing the memory-specific TimelineMarkersMemory component. Introduces a per-thread selector factory getTimelineMarkerIndexesBySchemaLocation(location) with a location-keyed cache of memoized selectors, plus a new TimelineMarkersCounter component that plugs it in. TimelineMarkersMemory stays untouched for TrackThread's "show memory markers on the main thread when there's no memory track" fallback; the corresponding CSS rule keeps its height so that path is unaffected until the follow-up issue #5962 retires it. --- src/components/timeline/Markers.tsx | 50 +++++++++++++++++++ src/components/timeline/TrackCounter.css | 1 + src/components/timeline/TrackCounter.tsx | 20 +++++--- src/selectors/per-thread/markers.ts | 33 ++++++++++++ .../__snapshots__/LocalTrack.test.tsx.snap | 4 +- .../__snapshots__/TrackMemory.test.tsx.snap | 4 +- 6 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/components/timeline/Markers.tsx b/src/components/timeline/Markers.tsx index 5a9cd953f6..55acfef03f 100644 --- a/src/components/timeline/Markers.tsx +++ b/src/components/timeline/Markers.tsx @@ -658,3 +658,53 @@ export const TimelineMarkersIPC = explicitConnect< mapDispatchToProps: { changeRightClickedMarker }, component: withSize(TimelineMarkers), }); + +/** + * Marker row rendered above a counter track's graph. Filters markers by the + * schema location declared on the counter, so any counter type can host its + * own marker row without the component having to know which category it is. + */ +type TimelineMarkersCounterOwnProps = OwnProps & { + readonly markerSchemaLocation: string; +}; + +// Drops the counter-specific own prop before forwarding to the base markers +// component, which doesn't know about markerSchemaLocation. +const _SizedTimelineMarkers = withSize(TimelineMarkers); +function _TimelineMarkersCounterInner( + props: ConnectedProps< + TimelineMarkersCounterOwnProps, + StateProps, + DispatchProps + > +) { + const { markerSchemaLocation: _unused, ...rest } = props; + return <_SizedTimelineMarkers {...rest} />; +} + +export const TimelineMarkersCounter = explicitConnect< + TimelineMarkersCounterOwnProps, + StateProps, + DispatchProps +>({ + mapStateToProps: (state, props) => { + const { threadsKey, markerSchemaLocation } = props; + const selectors = getThreadSelectorsFromThreadsKey(threadsKey); + const selectedThreads = getSelectedThreadIndexes(state); + + return { + getMarker: selectors.getMarkerGetter(state), + markerIndexes: + selectors.getTimelineMarkerIndexesBySchemaLocation( + markerSchemaLocation + )(state), + isSelected: _getTimelineMarkersIsSelected(selectedThreads, threadsKey), + isModifyingSelection: getPreviewSelectionIsBeingModified(state), + additionalClassName: 'timelineMarkersCounter', + testId: 'TimelineMarkersCounter', + rightClickedMarker: selectors.getRightClickedMarker(state), + }; + }, + mapDispatchToProps: { changeRightClickedMarker }, + component: _TimelineMarkersCounterInner, +}); diff --git a/src/components/timeline/TrackCounter.css b/src/components/timeline/TrackCounter.css index da8996786e..a3c54b66e9 100644 --- a/src/components/timeline/TrackCounter.css +++ b/src/components/timeline/TrackCounter.css @@ -35,6 +35,7 @@ font-weight: bold; } +.timelineMarkersCounter, .timelineMarkersMemory { height: 15px; opacity: 1; diff --git a/src/components/timeline/TrackCounter.tsx b/src/components/timeline/TrackCounter.tsx index bae0d74c49..2196b20fce 100644 --- a/src/components/timeline/TrackCounter.tsx +++ b/src/components/timeline/TrackCounter.tsx @@ -8,7 +8,7 @@ import { getCommittedRange, getCounterSelectors, } from 'firefox-profiler/selectors/profile'; -import { TimelineMarkersMemory } from './Markers'; +import { TimelineMarkersCounter } from './Markers'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; import { TrackCounterGraph } from './TrackCounterGraph'; import { @@ -34,7 +34,7 @@ type StateProps = { readonly threadIndex: ThreadIndex; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; - readonly hasMarkers: boolean; + readonly markerSchemaLocation: string | null; }; type DispatchProps = { @@ -54,16 +54,22 @@ export class TrackCounterImpl extends React.PureComponent { }; override render() { - const { counterIndex, rangeStart, rangeEnd, threadIndex, hasMarkers } = - this.props; + const { + counterIndex, + rangeStart, + rangeEnd, + threadIndex, + markerSchemaLocation, + } = this.props; return (
- {hasMarkers ? ( - ) : null} @@ -91,7 +97,7 @@ export const TrackCounter = explicitConnect< threadIndex: counter.mainThreadIndex, rangeStart: start, rangeEnd: end, - hasMarkers: Boolean(counter.display.markerSchemaLocation), + markerSchemaLocation: counter.display.markerSchemaLocation, }; }, mapDispatchToProps: { updatePreviewSelection }, diff --git a/src/selectors/per-thread/markers.ts b/src/selectors/per-thread/markers.ts index 8f083a15a7..51ac5bee7d 100644 --- a/src/selectors/per-thread/markers.ts +++ b/src/selectors/per-thread/markers.ts @@ -21,6 +21,7 @@ import type { MarkerIndex, Marker, MarkerSchema, + MarkerDisplayLocation, MarkerTiming, MarkerTimingAndBuckets, DerivedMarkerInfo, @@ -516,6 +517,37 @@ export function getMarkerSelectorsPerThread( MarkerData.filterMarkerByDisplayLocation ); + /** + * Returns markers for an arbitrary schema location. The inner selectors are + * memoized per location string so repeated lookups with the same location + * reuse a single reselect instance. + */ + const _timelineMarkerIndexesSelectorsBySchemaLocation: Map< + string, + Selector + > = new Map(); + const getTimelineMarkerIndexesBySchemaLocation = ( + schemaLocation: string + ): Selector => { + let selector = + _timelineMarkerIndexesSelectorsBySchemaLocation.get(schemaLocation); + if (selector === undefined) { + selector = createSelector( + getMarkerGetter, + getCommittedRangeFilteredMarkerIndexes, + ProfileSelectors.getMarkerSchema, + ProfileSelectors.getMarkerSchemaByName, + () => schemaLocation as MarkerDisplayLocation, + MarkerData.filterMarkerByDisplayLocation + ); + _timelineMarkerIndexesSelectorsBySchemaLocation.set( + schemaLocation, + selector + ); + } + return selector; + }; + /** * This organizes the network markers in rows so that they're nicely displayed * in the header. @@ -783,6 +815,7 @@ export function getMarkerSelectorsPerThread( getTimelineFileIoMarkerIndexes, getTimelineMemoryMarkerIndexes, getTimelineIPCMarkerIndexes, + getTimelineMarkerIndexesBySchemaLocation, getNetworkTrackTiming, getRangeFilteredScreenshotsById, getSearchFilteredMarkerIndexes, diff --git a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap index c6a9af08e0..9e70f8ed6e 100644 --- a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap @@ -30,8 +30,8 @@ exports[`timeline/LocalTrack with a memory track matches the snapshot of the mem class="timelineTrackCounter" >
Date: Fri, 24 Apr 2026 11:46:14 +0200 Subject: [PATCH 7/8] Incorporate code review feedback - computeLocalTracksByPid no longer drops power counters with at most two samples. - Counter-counter tie-break: keepProfileThreadOrder takes precedence over display.sortWeight in _getDefaultLocalTrackOrder. - Reorder the v62 processed-profile upgrader branches to match the sortWeight-ascending layout in _deriveCounterDisplay. - Drive --graph-height and --markers-height on the TrackCounter wrapper from the corresponding TS constants. - Accept 'counter' in TrackContextMenu's ALLOWED_TYPES. - Document why event-delay and process-cpu have no entries in the v16 upgrader's slot maps. --- src/app-logic/url-handling.ts | 19 +++++++----------- src/components/timeline/TrackContextMenu.tsx | 2 +- src/components/timeline/TrackCounter.css | 4 ++-- src/components/timeline/TrackCounter.tsx | 11 +++++++++- src/components/timeline/TrackCounterGraph.tsx | 2 +- .../processed-profile-versioning.ts | 20 +++++++++---------- src/profile-logic/tracks.ts | 18 +++++++---------- .../__snapshots__/LocalTrack.test.tsx.snap | 1 + .../TrackBandwidth.test.tsx.snap | 1 + .../__snapshots__/TrackMemory.test.tsx.snap | 1 + .../__snapshots__/TrackPower.test.tsx.snap | 1 + .../TrackProcessCPU.test.tsx.snap | 1 + 12 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/app-logic/url-handling.ts b/src/app-logic/url-handling.ts index 0c7adeb457..19ba9d3de4 100644 --- a/src/app-logic/url-handling.ts +++ b/src/app-logic/url-handling.ts @@ -1439,7 +1439,9 @@ const _upgraders: { function _computeV16LocalTrackIndexRemap( profile: Profile ): Map> { - // Pre-v16 (main) LOCAL_TRACK_INDEX_ORDER. Frozen snapshot — do not change. + // Pre-v16 LOCAL_TRACK_INDEX_ORDER. + // Note: we don't preserve 'event-delay' and 'process-cpu' in URLs, + // as they are experimental. const OLD_SLOT = { thread: 0, network: 1, @@ -1449,8 +1451,7 @@ function _computeV16LocalTrackIndexRemap( power: 6, bandwidth: 8, }; - // Post-v16 LOCAL_TRACK_INDEX_ORDER. Frozen snapshot — do not change even if - // production LOCAL_TRACK_INDEX_ORDER evolves later. + // Post-v16 LOCAL_TRACK_INDEX_ORDER. const NEW_SLOT = { thread: 0, network: 1, @@ -1555,7 +1556,7 @@ function _computeV16LocalTrackIndexRemap( } else if (category === 'Bandwidth') { oldSlot = OLD_SLOT.bandwidth; } - // We assume there is no data when <= 2 samples + // We assumed there was no data when <= 2 samples. else if (category === 'power' && samples.length > 2) { oldSlot = OLD_SLOT.power; } else { @@ -1565,14 +1566,8 @@ function _computeV16LocalTrackIndexRemap( // NEW behavior: mirror computeLocalTracksByPid. processCPU counters are // added later by addProcessCPUTracksForProcess when the experimental // toggle fires, every other counter becomes a track. - let newSlot: number | null; - if (category === 'CPU' && name === 'processCPU') { - newSlot = null; - } else if (category === 'power' && samples.length <= 2) { - newSlot = null; - } else { - newSlot = NEW_SLOT.counter; - } + const newSlot: number | null = + category === 'CPU' && name === 'processCPU' ? null : NEW_SLOT.counter; if (oldSlot === null && newSlot === null) { continue; diff --git a/src/components/timeline/TrackContextMenu.tsx b/src/components/timeline/TrackContextMenu.tsx index d4939d3526..eb17df155e 100644 --- a/src/components/timeline/TrackContextMenu.tsx +++ b/src/components/timeline/TrackContextMenu.tsx @@ -936,7 +936,7 @@ class TimelineTrackContextMenuImpl extends PureComponent< const ALLOWED_TYPES = [ 'screenshots', - 'memory', + 'counter', 'network', 'ipc', 'event-delay', diff --git a/src/components/timeline/TrackCounter.css b/src/components/timeline/TrackCounter.css index a3c54b66e9..f76deda599 100644 --- a/src/components/timeline/TrackCounter.css +++ b/src/components/timeline/TrackCounter.css @@ -5,7 +5,7 @@ .timelineTrackCounterGraph { position: relative; width: 100%; - height: 25px; + height: var(--graph-height); } .timelineTrackCounterCanvas { @@ -37,6 +37,6 @@ .timelineMarkersCounter, .timelineMarkersMemory { - height: 15px; + height: var(--markers-height); opacity: 1; } diff --git a/src/components/timeline/TrackCounter.tsx b/src/components/timeline/TrackCounter.tsx index 2196b20fce..6b07b30adc 100644 --- a/src/components/timeline/TrackCounter.tsx +++ b/src/components/timeline/TrackCounter.tsx @@ -13,6 +13,7 @@ import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; import { TrackCounterGraph } from './TrackCounterGraph'; import { TRACK_COUNTER_GRAPH_HEIGHT, + TRACK_COUNTER_MARKERS_HEIGHT, TRACK_COUNTER_LINE_WIDTH, } from 'firefox-profiler/app-logic/constants'; @@ -63,7 +64,15 @@ export class TrackCounterImpl extends React.PureComponent { } = this.props; return ( -
+
{markerSchemaLocation !== null ? ( { // 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, { + const co2eq = this._co2.perByteTrace(bytes, false, { gridIntensity: { device: 0 }, }); const carbonValue = formatNumber( diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index c8d94509d9..725b597997 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3059,7 +3059,16 @@ const _upgraders: { continue; } const { category, name } = counter; - if (category === 'Memory') { + if (category === 'Bandwidth') { + counter.display = { + graphType: 'line-rate', + unit: 'bytes', + color: counter.color ?? 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'Memory') { counter.display = { graphType: 'line-accumulated', unit: 'bytes', @@ -3077,15 +3086,6 @@ const _upgraders: { sortWeight: 30, label: name, }; - } else if (category === 'Bandwidth') { - counter.display = { - graphType: 'line-rate', - unit: 'bytes', - color: counter.color ?? 'blue', - markerSchemaLocation: null, - sortWeight: 10, - label: 'Bandwidth', - }; } else if (category === 'CPU' && name === 'processCPU') { counter.display = { graphType: 'line-rate', diff --git a/src/profile-logic/tracks.ts b/src/profile-logic/tracks.ts index 0d8ce5c393..ea5c690bbb 100644 --- a/src/profile-logic/tracks.ts +++ b/src/profile-logic/tracks.ts @@ -105,6 +105,9 @@ function _getDefaultLocalTrackOrder( profile && profile.counters ) { + if (profile.meta.keepProfileThreadOrder) { + return tracks[a].counterIndex - tracks[b].counterIndex; + } const counterA = profile.counters[tracks[a].counterIndex]; const counterB = profile.counters[tracks[b].counterIndex]; const sortWeightDiff = @@ -112,9 +115,6 @@ function _getDefaultLocalTrackOrder( if (sortWeightDiff !== 0) { return sortWeightDiff; } - if (profile.meta.keepProfileThreadOrder) { - return tracks[a].counterIndex - tracks[b].counterIndex; - } return naturalSort.compare(counterA.name, counterB.name); } @@ -399,7 +399,7 @@ export function computeLocalTracksByPid( const { counters } = profile; if (counters) { for (let counterIndex = 0; counterIndex < counters.length; counterIndex++) { - const { pid, category, name, samples } = counters[counterIndex]; + const { pid, category, name } = counters[counterIndex]; if (!availablePids.has(pid)) { // If the global track is filtered out ignore it here too. continue; @@ -411,11 +411,6 @@ export function computeLocalTracksByPid( continue; } - if (category === 'power' && samples.length <= 2) { - // If we have only 2 samples, they are likely both 0 and we don't have a real counter. - continue; - } - let tracks = localTracksByPid.get(pid); if (tracks === undefined) { tracks = []; @@ -1572,8 +1567,6 @@ export function getSearchFilteredLocalTracksByPid( break; } case 'counter': { - // Match against the counter's display label (e.g., "Memory", - // "Bandwidth") rather than the generic type string 'counter'. const trackName = localTrackNames[trackIndex]; if (searchRegExp.test(trackName)) { searchFilteredLocalTracks.add(trackIndex); @@ -1761,6 +1754,9 @@ export function getTrackReferenceFromThreadIndex( * If the track is not a thread, some of them can be visible by default and some * of them can be hidden to reduce the noise. This mostly depends on either the * usefulness or the activity of that track. + * + * TODO: Check the counter track activity here to decide if it should be visible, + * see https://github.com/firefox-devtools/profiler/issues/5967. */ function _isLocalTrackVisible( localTrack: LocalTrack, diff --git a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap index 9e70f8ed6e..6247499fde 100644 --- a/src/test/components/__snapshots__/LocalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/LocalTrack.test.tsx.snap @@ -28,6 +28,7 @@ exports[`timeline/LocalTrack with a memory track matches the snapshot of the mem >
Date: Mon, 27 Apr 2026 10:19:15 +0200 Subject: [PATCH 8/8] Type markerSchemaLocation as MarkerDisplayLocation and reuse the schema-location selector Tightens the marker schema location field to MarkerDisplayLocation throughout the counter display config and consumers, removing the lingering string cast in the per-thread selector. The dedicated memory/IPC marker selectors are now built on top of getTimelineMarkerIndexesBySchemaLocation so there is a single source of truth for the schema-location filtering. --- src/components/timeline/Markers.tsx | 3 +- src/components/timeline/TrackCounter.tsx | 3 +- src/selectors/per-thread/markers.ts | 40 ++++++------------------ src/types/profile.ts | 9 ++++-- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/src/components/timeline/Markers.tsx b/src/components/timeline/Markers.tsx index 55acfef03f..536f3e4e2b 100644 --- a/src/components/timeline/Markers.tsx +++ b/src/components/timeline/Markers.tsx @@ -27,6 +27,7 @@ import type { CssPixels, Marker, MarkerIndex, + MarkerDisplayLocation, ThreadsKey, } from 'firefox-profiler/types'; @@ -665,7 +666,7 @@ export const TimelineMarkersIPC = explicitConnect< * own marker row without the component having to know which category it is. */ type TimelineMarkersCounterOwnProps = OwnProps & { - readonly markerSchemaLocation: string; + readonly markerSchemaLocation: MarkerDisplayLocation; }; // Drops the counter-specific own prop before forwarding to the base markers diff --git a/src/components/timeline/TrackCounter.tsx b/src/components/timeline/TrackCounter.tsx index 6b07b30adc..effc64db1d 100644 --- a/src/components/timeline/TrackCounter.tsx +++ b/src/components/timeline/TrackCounter.tsx @@ -21,6 +21,7 @@ import type { CounterIndex, ThreadIndex, Milliseconds, + MarkerDisplayLocation, } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; @@ -35,7 +36,7 @@ type StateProps = { readonly threadIndex: ThreadIndex; readonly rangeStart: Milliseconds; readonly rangeEnd: Milliseconds; - readonly markerSchemaLocation: string | null; + readonly markerSchemaLocation: MarkerDisplayLocation | null; }; type DispatchProps = { diff --git a/src/selectors/per-thread/markers.ts b/src/selectors/per-thread/markers.ts index 51ac5bee7d..27d5b86483 100644 --- a/src/selectors/per-thread/markers.ts +++ b/src/selectors/per-thread/markers.ts @@ -492,42 +492,17 @@ export function getMarkerSelectorsPerThread( MarkerData.filterMarkerByDisplayLocation ); - /** - * This returns only memory markers. - */ - const getTimelineMemoryMarkerIndexes: Selector = - createSelector( - getMarkerGetter, - getCommittedRangeFilteredMarkerIndexes, - ProfileSelectors.getMarkerSchema, - ProfileSelectors.getMarkerSchemaByName, - () => 'timeline-memory' as const, - MarkerData.filterMarkerByDisplayLocation - ); - - /** - * This returns only IPC markers. - */ - const getTimelineIPCMarkerIndexes: Selector = createSelector( - getMarkerGetter, - getCommittedRangeFilteredMarkerIndexes, - ProfileSelectors.getMarkerSchema, - ProfileSelectors.getMarkerSchemaByName, - () => 'timeline-ipc' as const, - MarkerData.filterMarkerByDisplayLocation - ); - /** * Returns markers for an arbitrary schema location. The inner selectors are - * memoized per location string so repeated lookups with the same location - * reuse a single reselect instance. + * memoized per location so repeated lookups with the same location reuse a + * single reselect instance. */ const _timelineMarkerIndexesSelectorsBySchemaLocation: Map< - string, + MarkerDisplayLocation, Selector > = new Map(); const getTimelineMarkerIndexesBySchemaLocation = ( - schemaLocation: string + schemaLocation: MarkerDisplayLocation ): Selector => { let selector = _timelineMarkerIndexesSelectorsBySchemaLocation.get(schemaLocation); @@ -537,7 +512,7 @@ export function getMarkerSelectorsPerThread( getCommittedRangeFilteredMarkerIndexes, ProfileSelectors.getMarkerSchema, ProfileSelectors.getMarkerSchemaByName, - () => schemaLocation as MarkerDisplayLocation, + () => schemaLocation, MarkerData.filterMarkerByDisplayLocation ); _timelineMarkerIndexesSelectorsBySchemaLocation.set( @@ -548,6 +523,11 @@ export function getMarkerSelectorsPerThread( return selector; }; + const getTimelineMemoryMarkerIndexes = + getTimelineMarkerIndexesBySchemaLocation('timeline-memory'); + const getTimelineIPCMarkerIndexes = + getTimelineMarkerIndexesBySchemaLocation('timeline-ipc'); + /** * This organizes the network markers in rows so that they're nicely displayed * in the header. diff --git a/src/types/profile.ts b/src/types/profile.ts index a4072ca72e..04d8a2ee79 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -3,7 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { Milliseconds, Address, Microseconds, Bytes } from './units'; -import type { MarkerPayload, MarkerSchema, MarkerFormatType } from './markers'; +import type { + MarkerPayload, + MarkerSchema, + MarkerFormatType, + MarkerDisplayLocation, +} from './markers'; import type { MarkerPhase, ProfilingLog } from './gecko-profile'; export type IndexIntoStackTable = number; @@ -540,7 +545,7 @@ export type CounterDisplayConfig = { color: GraphColor; // The marker schema display location to filter markers for this track, // e.g., "timeline-memory". If null, no markers are shown. - markerSchemaLocation: string | null; + markerSchemaLocation: MarkerDisplayLocation | null; // Controls the default display position of this counter track relative to // other tracks. Tracks with lower values appear closer to the top. sortWeight: number;