Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 65 additions & 21 deletions src/components/timeline/TrackPowerGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -129,34 +130,75 @@ class TrackPowerCanvas extends React.PureComponent<CanvasProps> {
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So differently, we are rounding the numbers with this patch. I guess this is going to change the shape of the graph a bit right? But otherwise it's pretty much impossible to implement this without rounding, and it looks like the shape doesn't change that much, so looks good to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even when switching back and forth between the same profile on profiler.firefox.com and on my local version with the patch applied, I couldn't see any difference in the shape of the graph. The deviceWidth is width * devicePixelRatio, so any integer x value here is already a half pixel on a hidpi screen. I think that's the reason why there's no visible difference.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be also good to mention the "min/max decimation" name for future reference and also maybe we can put the link that Julien mentioned in his comment? First I was a bit confused with the algorithm but looking at the description made it more obvious (I was confused with how many points we draw for each pixel)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding "with max-min decimation algorithm" to the wording of the comment. I was hoping "we only draw the min and max values, to draw a vertical line covering all the other sample values." in my comment was descriptive enough of how the algorithm worked, but I guess not. Thanks for suggesting the comment improvement.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for inproving the comment!

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);
Expand Down Expand Up @@ -192,7 +234,9 @@ class TrackPowerCanvas extends React.PureComponent<CanvasProps> {

const canvas = this._canvas;
if (canvas) {
this.drawCanvas(canvas);
timeCode('TrackPowerCanvas render', () => {
this.drawCanvas(canvas);
});
}
}

Expand Down
18 changes: 13 additions & 5 deletions src/test/components/TrackPower.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -60,14 +60,22 @@ 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,
threadIndex,
{
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',
Expand Down Expand Up @@ -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(
Expand Down
46 changes: 33 additions & 13 deletions src/test/components/__snapshots__/TrackPower.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ exports[`TrackPower has a tooltip that matches the snapshot 1`] = `
Energy used in the visible range
:
</div>
7.2⁩ µWh (⁨0.003⁩ mg CO₂e)
8.4⁩ µWh (⁨0.004⁩ mg CO₂e)
</div>
</div>
`;
Expand All @@ -38,7 +38,7 @@ Array [
"clearRect",
0,
0,
80,
120,
25,
],
Array [
Expand Down Expand Up @@ -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",
Expand All @@ -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 [
Expand All @@ -136,7 +156,7 @@ exports[`TrackPower matches the component snapshot 1`] = `
<canvas
class="timelineTrackPowerCanvas"
height="25"
width="80"
width="120"
/>
</div>
<div
Expand Down