Skip to content

Commit

Permalink
Add the ability to gather power usage measurements on Android from US…
Browse files Browse the repository at this point in the history
…B power meters. (#2134)

This PR adds the ability to gather power from USB power meters that are supported by the usb-power-profiling module: https://github.com/fqueze/usb-power-profiling

The capability is added to both Firefox, and Chromium-based browsers. Some methods are added to the Android class to make it simpler to parse the power usage measurements, and also obtain a full "power profile" of the power usage throughout the full test. The power profile is gathered for each test-iteration/url measured.
  • Loading branch information
gmierz committed May 28, 2024
1 parent fedd6d8 commit 0007137
Show file tree
Hide file tree
Showing 6 changed files with 721 additions and 15 deletions.
37 changes: 37 additions & 0 deletions lib/android/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { EOL as endOfLine } from 'node:os';
import { execa } from 'execa';
import intel from 'intel';
import pkg from '@devicefarmer/adbkit';
import usbPowerProfiler from 'usb-power-profiling/usb-power-profiling.js';
const { Adb } = pkg;
import get from 'lodash.get';
import { pathToFolder } from '../support/pathToFolder.js';
const log = intel.getLogger('browsertime.android');
const mkdir = promisify(_mkdir);
const delay = ms => new Promise(res => setTimeout(res, ms));
Expand Down Expand Up @@ -466,6 +468,41 @@ export class Android {
const batterystats = await this._runCommandAndGet('dumpsys batterystats');
return parsePowerMetrics(batterystats, packageName);
}

async measureUsbPowerUsage(startTime, endTime) {
return getUsbPowerUsage(startTime, endTime);
}

async getUsbPowerUsageProfile(index, url, result, options, storageManager) {
let profileData = await usbPowerProfiler.profileFromData();
let destinationFilename = join(
await pathToFolder(url, options),
`powerProfile-${index}.json`
);

await storageManager.writeJson(destinationFilename, profileData);
}
}

async function getUsbPowerUsage(startTime, endTime) {
let baselineUsageData = await usbPowerProfiler.getPowerData(
startTime - 2,
endTime - 1
);
let baselineUsageTotal = baselineUsageData[0]['samples']['data'].reduce(
(currSum, currVal) => currSum + Number.parseInt(currVal[1]),
0
);
let baselineUsage =
baselineUsageTotal / baselineUsageData[0]['samples']['data'].length;

let powerUsageData = await usbPowerProfiler.getPowerData(startTime, endTime);
let powerUsage = powerUsageData[0]['samples']['data'].reduce(
(currSum, currVal) => currSum + Number.parseInt(currVal[1]),
0
);

return { powerUsage, baselineUsage };
}

async function parsePowerMetrics(batterystats, packageName) {
Expand Down
39 changes: 33 additions & 6 deletions lib/chrome/webdriver/chromium.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { unlink as _unlink, rm as _rm } from 'node:fs';
import { join } from 'node:path';
import { logging } from 'selenium-webdriver';
import intel from 'intel';
import usbPowerProfiler from 'usb-power-profiling/usb-power-profiling.js';
const log = intel.getLogger('browsertime.chrome');
const { Type } = logging;
import { longTaskMetrics } from '../longTaskMetrics.js';
Expand Down Expand Up @@ -36,6 +37,7 @@ export class Chromium {
// Keep the HAR file for all runs
this.hars = [];
this.androidTmpDir = '/data/local/tmp/';
this.testStartTime = undefined;
}

/**
Expand Down Expand Up @@ -68,6 +70,10 @@ export class Chromium {
// https://github.com/cyrus-and/chrome-remote-interface/issues/332
if (isAndroidConfigured(this.options)) {
await this.android.addDevtoolsFw();

if (this.options.androidUsbPower) {
usbPowerProfiler.startSampling();
}
}

this.cdpClient = new ChromeDevtoolsProtocol(this.options);
Expand Down Expand Up @@ -153,8 +159,12 @@ export class Chromium {
log.info('Could not set cookie because the URL is unknown');
}

if (this.android && this.options.androidPower) {
await this.android.resetPowerUsage();
if (this.android) {
if (this.options.androidPower) {
await this.android.resetPowerUsage();
} else if (this.options.androidUsbPower) {
await usbPowerProfiler.resetPowerData();
}
}

if (
Expand All @@ -165,6 +175,8 @@ export class Chromium {
this.isTracing = true;
return this.cdpClient.startTrace();
}

this.testStartTime = Date.now();
}

/**
Expand Down Expand Up @@ -218,10 +230,25 @@ export class Chromium {
'GET_LONG_TASKS'
);

if (this.android && this.options.androidPower) {
result.power = await this.android.measurePowerUsage(
this.chrome.android.package
);
if (this.android) {
if (this.options.androidPower) {
result.power = await this.android.measurePowerUsage(
this.chrome.android.package
);
} else if (this.options.androidUsbPower) {
result.power = await this.android.measureUsbPowerUsage(
this.testStartTime,
Date.now()
);

await this.android.getUsbPowerUsageProfile(
index,
url,
result,
this.options,
this.storageManager
);
}
}

if (this.options.verbose >= 2 || this.chrome.enableChromeDriverLog) {
Expand Down
39 changes: 33 additions & 6 deletions lib/firefox/webdriver/firefox.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { promisify } from 'node:util';
import { join } from 'node:path';
import intel from 'intel';
import get from 'lodash.get';
import usbPowerProfiler from 'usb-power-profiling/usb-power-profiling.js';
import { adapters } from 'ff-test-bidi-har-export';
import { getEmptyHAR, mergeHars } from '../../support/har/index.js';
import { pathToFolder } from '../../support/pathToFolder.js';
Expand Down Expand Up @@ -32,6 +33,7 @@ export class Firefox {
this.aliasAndUrl = {};
// This keep the HAR files for all runs
this.hars = [];
this.testStartTime = undefined;
}

/**
Expand Down Expand Up @@ -71,6 +73,10 @@ export class Firefox {
runner.getDriver(),
this.options
);

if (isAndroidConfigured(this.options) && this.options.androidUsbPower) {
usbPowerProfiler.startSampling();
}
}

/**
Expand Down Expand Up @@ -131,8 +137,12 @@ export class Firefox {
await this.har.startRecording();
}

if (isAndroidConfigured(this.options) && this.options.androidPower) {
await this.android.resetPowerUsage();
if (isAndroidConfigured(this.options)) {
if (this.options.androidPower) {
await this.android.resetPowerUsage();
} else if (this.options.androidUsbPower) {
await usbPowerProfiler.resetPowerData();
}
}

if (
Expand All @@ -158,6 +168,8 @@ export class Firefox {
this.perfStats = new PerfStats(runner, this.firefoxConfig);
return this.perfStats.start();
}

this.testStartTime = Date.now();
}

/**
Expand All @@ -169,10 +181,25 @@ export class Firefox {
async afterPageCompleteCheck(runner, index, url, alias) {
const result = { url, alias };

if (isAndroidConfigured(this.options) && this.options.androidPower) {
result.power = await this.android.measurePowerUsage(
this.firefoxConfig.android.package
);
if (isAndroidConfigured(this.options)) {
if (this.options.androidPower) {
result.power = await this.android.measurePowerUsage(
this.firefoxConfig.android.package
);
} else if (this.options.androidUsbPower) {
result.power = await this.android.measureUsbPowerUsage(
this.testStartTime,
Date.now()
);

await this.android.getUsbPowerUsageProfile(
index,
url,
result,
this.options,
this.storageManager
);
}
}

if (
Expand Down
9 changes: 9 additions & 0 deletions lib/support/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ export function parseCommandLine() {
'(You have to disable charging yourself for this - it depends on the phone model).',
group: 'android'
})
.option('android.usbPowerTesting', {
alias: 'androidUsbPower',
type: 'boolean',
describe:
'Enables android power testing using usb-power-profiling. Assumes that ' +
'a valid device is attached to the phone. See here for supported devices: ' +
'https://github.com/fqueze/usb-power-profiling?tab=readme-ov-file#supported-devices',
group: 'android'
})
.option('chrome.CPUThrottlingRate', {
type: 'number',
describe:
Expand Down
Loading

0 comments on commit 0007137

Please sign in to comment.