diff --git a/lib/core/engine/collector.js b/lib/core/engine/collector.js index d3449c01b..54c36e0f3 100644 --- a/lib/core/engine/collector.js +++ b/lib/core/engine/collector.js @@ -426,6 +426,17 @@ export class Collector { results.geckoPerfStats.push(data.perfStats); } + // Add Firefox cpu power + if (data.powerConsumption) { + if (!results.powerConsumption) { + results.powerConsumption = []; + } + results.powerConsumption.push(data.powerConsumption); + statistics.addDeep({ + powerConsumption: results.powerConsumption + }); + } + // Add total memory if (this.options.firefox && this.options.firefox.memoryReport) { statistics.addDeep({ diff --git a/lib/core/engine/command/geckoProfiler.js b/lib/core/engine/command/geckoProfiler.js index b10ad03e1..c64640794 100644 --- a/lib/core/engine/command/geckoProfiler.js +++ b/lib/core/engine/command/geckoProfiler.js @@ -26,7 +26,11 @@ export class GeckoProfiler { async stop() { if (this.options.browser === 'firefox') { if (this.options.firefox.geckoProfilerRecordingType === 'custom') { - return this.GeckoProfiler.stop(this.index, this.result[0].url); + return this.GeckoProfiler.stop( + this.index, + this.result[0].url, + this.result + ); } } else { throw new Error('Geckoprofiler only works in Firefox'); diff --git a/lib/firefox/geckoProfiler.js b/lib/firefox/geckoProfiler.js index 77fbd2f36..ae9b28fd2 100644 --- a/lib/firefox/geckoProfiler.js +++ b/lib/firefox/geckoProfiler.js @@ -8,6 +8,26 @@ import { BrowserError } from '../support/errors.js'; const delay = ms => new Promise(res => setTimeout(res, ms)); const log = intel.getLogger('browsertime.firefox'); +// Return power usage in Wh +function computePowerSum(counter) { + let sum = 0; + // Older Firefoxes see https://github.com/sitespeedio/sitespeed.io/issues/3944#issuecomment-1871090793 + if (counter.sample_groups) { + for (const groups of counter.sample_groups) { + const countIndex = groups.samples.schema.count; + for (const sample of groups.samples.data) { + sum += sample[countIndex]; + } + } + } else { + const countIndex = counter.samples.schema.count; + for (const sample of counter.samples.data) { + sum += sample[countIndex]; + } + } + return sum * 1e-12; +} + /** * Timeout a promise after ms. Use promise.race to compete * about the timeout and the promise. @@ -130,7 +150,7 @@ export class GeckoProfiler { return delay(firefoxConfig.geckoProfilerSettleTime || 3000); } - async stop(index, url) { + async stop(index, url, result) { const runner = this.runner; const storageManager = this.storageManager; const options = this.options; @@ -183,6 +203,34 @@ export class GeckoProfiler { await android._downloadFile(deviceProfileFilename, destinationFilename); } + if (this.firefoxConfig.powerConsumption === true) { + const chosenFeatures = get( + this.firefoxConfig, + 'geckoProfilerParams.features', + geckoProfilerDefaults.features + ); + if (chosenFeatures.includes('power')) { + log.info('Collecting CPU power consumtion'); + const profile = JSON.parse( + await storageManager.readData( + `geckoProfile-${index}.json`, + join(pathToFolder(url, options)) + ) + ); + let power = 0; + for (const counter of profile.counters) { + if (counter.category === 'power') { + power += computePowerSum(counter); + } + } + result.powerConsumption = Number(power); + } else { + log.warning( + 'Missing power setting in geckoProfilerParams.features so power will not be collected' + ); + } + } + // GZIP the profile and remove the old file log.info('Gzip file the profile.'); const name = this.options.enableProfileRun diff --git a/lib/firefox/webdriver/firefox.js b/lib/firefox/webdriver/firefox.js index d5ddd8e33..a0753c1b9 100644 --- a/lib/firefox/webdriver/firefox.js +++ b/lib/firefox/webdriver/firefox.js @@ -140,7 +140,7 @@ export class Firefox { this.firefoxConfig.geckoProfiler && this.firefoxConfig.geckoProfilerRecordingType !== 'custom' ) { - await this.geckoProfiler.stop(index, url); + await this.geckoProfiler.stop(index, url, result); } if (this.firefoxConfig.perfStats) { diff --git a/lib/support/cli.js b/lib/support/cli.js index 2ad3b4636..cf29d4286 100644 --- a/lib/support/cli.js +++ b/lib/support/cli.js @@ -537,6 +537,13 @@ export function parseCommandLine() { 'need to specify the logs you wish to gather.', group: 'firefox' }) + .option('firefox.powerConsumption', { + type: 'boolean', + default: false, + describe: + 'Enable power consumption collection (in Wh). To get the consumption you also need to set firefox.geckoProfilerParams.features to include power.', + group: 'firefox' + }) .option('firefox.setMozLog', { describe: 'Use in conjunction with firefox.collectMozLog to set MOZ_LOG to something ' + diff --git a/lib/support/statistics.js b/lib/support/statistics.js index 095df3f49..ef141a033 100644 --- a/lib/support/statistics.js +++ b/lib/support/statistics.js @@ -10,6 +10,10 @@ function validateType(value, type, message) { } } +function hasDecimals(num) { + return num !== Math.floor(num); +} + function percentileName(percentile) { if (percentile === 0) { return 'min'; @@ -129,7 +133,7 @@ export class Statistics { // https://en.wikipedia.org/wiki/Interquartile_range stats = stats.iqr(); } - if (stats.median() < 1 && stats.median() > 0) { + if (stats.median() < 10 && stats.median() > 0) { decimals = 4; } const node = { @@ -165,6 +169,12 @@ export class Statistics { } if (stats.median() < 1 && stats.median() > 0) { decimals = 4; + } else if ( + hasDecimals(stats.median()) && + stats.median() < 100 && + stats.median() > 1 + ) { + decimals = 2; } const results = { median: Number.parseFloat(stats.median().toFixed(decimals)),