Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

misc: timings script #9723

Merged
merged 20 commits into from
Oct 2, 2019
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ last-run-results.html

latest-run
lantern-data
timings-data

closure-error.log
yarn-error.log
Expand Down
139 changes: 139 additions & 0 deletions lighthouse-core/scripts/timings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

// Example:
// node lighthouse-core/scripts/timings.js --name my-collection --collect -n 3 --lh-flags='--only-audits=unminified-javascript' --urls https://www.example.com https://www.nyt.com
// node lighthouse-core/scripts/timings.js --name my-collection --summarize --measure-filter 'loadPage|connect'

const fs = require('fs');
const {execSync} = require('child_process');
const yargs = require('yargs');

const LH_ROOT = `${__dirname}/../..`;
const ROOT_OUTPUT_DIR = `${LH_ROOT}/timings-data`;

const argv = yargs
.help('help')
.describe({
'name': 'Unique identifier, makes the folder for storing LHRs. Not a path',
// --collect
'collect': 'Saves LHRs to disk',
'lh-flags': 'Lighthouse flags',
'urls': 'Urls to run',
'n': 'Number of times to run',
// --summarize
'summarize': 'Prints statistics report',
'measure-filter': 'Regex filter of measures to report. Optional',
'output': 'table, json',
})
.string('measure-filter')
.default('output', 'table')
.array('urls')
.string('lh-flags')
.default('lh-flags', '')
// Why is the printing for examples so awful?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Anyone know how to make yargs examples output not look terrible?
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

We're still on ancient yargs so I'm sure it's been fixed in a version released in the past 5 years :)

// eslint-disable max-len
// .example('node lighthouse-core/scripts/timings.js --name my-collection --collect -n 3 --lh-flags=\'--only-audits=unminified-javascript\' --urls https://www.example.com', 'Collect')
// .example('node lighthouse-core/scripts/timings.js --name my-collection --summarize --measure-filter \'loadPage|connect\'', 'Summarize')
// eslint-enable max-len
.wrap(yargs.terminalWidth())
.argv;

const outputDir = `${ROOT_OUTPUT_DIR}/${argv.name}`;

if (argv.collect) {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
if (!fs.existsSync(ROOT_OUTPUT_DIR)) fs.mkdirSync(ROOT_OUTPUT_DIR);
if (fs.existsSync(outputDir)) throw new Error(`folder already exists: ${outputDir}`);
fs.mkdirSync(outputDir);

for (const url of argv.urls) {
for (let i = 0; i < argv.n; i++) {
const cmd = [
'node',
`${LH_ROOT}/lighthouse-cli`,
url,
`--output-path=${outputDir}/lhr-${url.replace(/[^a-zA-Z0-9]/g, '_')}-${i}.json`,
'--output=json',
argv.lhFlags,
].join(' ');
execSync(cmd, {stdio: 'ignore'});
}
}
}

/**
* @param {number[]} values
*/
function average(values) {
return values.reduce((sum, value) => sum + value) / values.length;
}

/**
* Round to the tenth.
* @param {number} value
*/
function round(value) {
return Math.round(value * 10) / 10;
}

if (argv.summarize) {
/** @type {Map<string, number[]>} */
const measuresMap = new Map();
/** @type {RegExp|null} */
const measureFilter = argv.measureFilter ? new RegExp(argv.measureFilter, 'i') : null;

for (const lhrPath of fs.readdirSync(outputDir)) {
const lhrJson = fs.readFileSync(`${outputDir}/${lhrPath}`, 'utf-8');
/** @type {LH.Result} */
const lhr = JSON.parse(lhrJson);

for (const measureName of lhr.timing.entries.map(entry => entry.name)) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: it might make sense to s/measure/timing for a lot of these names, to avoid splitting the nomenclature from timing/Execution timings/timingEntries we use for the values in all of the core files. Not a huge deal, but it does make it harder to remember in this file that there are accumulations of lhr.timing entries

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

gave it a shot

if (measureFilter && !measureFilter.test(measureName)) {
continue;
}

const measuresKey = `${lhr.requestedUrl}@@@${measureName}`;
let measures = measuresMap.get(measuresKey);
if (!measures) {
measures = [];
measuresMap.set(measuresKey, measures);
}

const measureEntry = lhr.timing.entries.find(measure => measure.name === measureName);
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
if (!measureEntry) throw new Error('missing measure');

measures.push(measureEntry.duration);
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
}
}

const results = [...measuresMap.entries()].map(([measuresKey, measures]) => {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
const [url, measureName] = measuresKey.split('@@@');
const mean = average(measures);
const min = Math.min(...measures);
const max = Math.max(...measures);
const stdev = Math.sqrt(average(measures.map(measure => (measure - mean) ** 2)));
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
return {
measure: measureName,
url,
n: measures.length,
mean: round(mean),
stdev: round(stdev),
min,
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
max,
};
}).sort((a, b) => {
return a.measure.localeCompare(b.measure);
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
});

if (argv.output === 'table') {
// eslint-disable-next-line no-console
console.table(results);
} else if (argv.output === 'json') {
// eslint-disable-next-line no-console
console.log(JSON.stringify(results, null, 2));
}
}