From 99c603e5a1f13fcef45232def5d96bbeee71d3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Sat, 27 May 2023 04:19:49 +0200 Subject: [PATCH] Draw power tracks faster by skipping samples that would be displayed on the same pixel. --- src/components/timeline/TrackPowerGraph.js | 86 ++++++++++++++----- src/test/components/TrackPower.test.js | 18 ++-- .../__snapshots__/TrackPower.test.js.snap | 46 +++++++--- 3 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/components/timeline/TrackPowerGraph.js b/src/components/timeline/TrackPowerGraph.js index 070b93b241..81a44bac68 100644 --- a/src/components/timeline/TrackPowerGraph.js +++ b/src/components/timeline/TrackPowerGraph.js @@ -33,6 +33,7 @@ import type { 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'; @@ -129,34 +130,75 @@ class TrackPowerCanvas extends React.PureComponent { 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 getX = (i) => + Math.round((samples.time[i] - rangeStart) * millisecondWidth); + const getPower = (i) => { const sampleTimeDeltaInMs = i === 0 ? interval : samples.time[i] - samples.time[i - 1]; - const unitGraphCount = - samples.count[i] / sampleTimeDeltaInMs / countRangePerMs; + return samples.count[i] / sampleTimeDeltaInMs; + }; + const getY = (rawY) => { + 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. - y = + return Math.round( 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 { + 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); @@ -192,7 +234,9 @@ class TrackPowerCanvas extends React.PureComponent { const canvas = this._canvas; if (canvas) { - this.drawCanvas(canvas); + timeCode('TrackPowerCanvas render', () => { + this.drawCanvas(canvas); + }); } } diff --git a/src/test/components/TrackPower.test.js b/src/test/components/TrackPower.test.js index afa609f3b5..d615a48781 100644 --- a/src/test/components/TrackPower.test.js +++ b/src/test/components/TrackPower.test.js @@ -35,7 +35,7 @@ import { autoMockElementSize } from '../fixtures/mocks/element-size'; import { autoMockIntersectionObserver } from '../fixtures/mocks/intersection-observer'; // The following constants determine the size of the drawn graph. -const SAMPLE_COUNT = 8; +const SAMPLE_COUNT = 12; const PIXELS_PER_SAMPLE = 10; const GRAPH_WIDTH = PIXELS_PER_SAMPLE * SAMPLE_COUNT; const GRAPH_HEIGHT = 10; @@ -60,6 +60,11 @@ describe('TrackPower', function () { const thread = profile.threads[threadIndex]; // Changing one of the sample times, so we can test different intervals. thread.samples.time[1] = 1.5; // It was 1 before. + // Ensure some samples are very close to each other, to exercise + // the max min decimation algorithm. + for (let i = 7; i < thread.samples.time.length - 1; ++i) { + thread.samples.time[i] = 7 + i / 100; + } profile.counters = [ getCounterForThreadWithSamples( thread, @@ -67,7 +72,10 @@ describe('TrackPower', function () { { time: thread.samples.time.slice(), // Power usage numbers. They are pWh so they are pretty big. - count: [10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 10000], + count: [ + 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, + 20000, 1, 12000, 100000, + ], length: SAMPLE_COUNT, }, 'SystemPower', @@ -197,10 +205,10 @@ describe('TrackPower', function () { expect(screen.getByText(/Power:/).nextSibling).toHaveTextContent( '360\u2069 mW' ); - // Over the full range, we get 7.240 µWh, therefore we'll see in the tooltip - // 7.2 µWh. + // Over the full range, we get 8.352 µWh, therefore we'll see in the tooltip + // 8.4 µWh. expect(screen.getByText(/visible range:/).nextSibling).toHaveTextContent( - '7.2\u2069 µWh' + '8.4\u2069 µWh' ); // Over the preview selection, we get 5 µWh which shows up as 5.0 µWh. expect( diff --git a/src/test/components/__snapshots__/TrackPower.test.js.snap b/src/test/components/__snapshots__/TrackPower.test.js.snap index 3e195d7595..cf0ff48b6b 100644 --- a/src/test/components/__snapshots__/TrackPower.test.js.snap +++ b/src/test/components/__snapshots__/TrackPower.test.js.snap @@ -27,7 +27,7 @@ exports[`TrackPower has a tooltip that matches the snapshot 1`] = ` Energy used in the visible range : - ⁨7.2⁩ µWh (⁨0.003⁩ mg CO₂e) + ⁨8.4⁩ µWh (⁨0.004⁩ mg CO₂e) `; @@ -38,7 +38,7 @@ Array [ "clearRect", 0, 0, - 80, + 120, 25, ], Array [ @@ -68,22 +68,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", @@ -93,24 +93,44 @@ Array [ Array [ "lineTo", 60, - 23.862, + 24, + ], + Array [ + "lineTo", + 71, + 20, + ], + Array [ + "lineTo", + 71, + 24, + ], + Array [ + "lineTo", + 71, + 15, ], Array [ "lineTo", - 70, - 23.954, + 71, + 18, ], Array [ "lineTo", - 80, - 23.954, + 110, + 24, + ], + Array [ + "lineTo", + 120, + 24, ], Array [ "stroke", ], Array [ "lineTo", - 80, + 120, 25, ], Array [ @@ -136,7 +156,7 @@ exports[`TrackPower matches the component snapshot 1`] = `