browsertime
Version:
Get performance metrics from your web page using Browsertime.
241 lines (218 loc) • 7.76 kB
JavaScript
import merge from 'lodash.merge';
import get from 'lodash.get';
import set from 'lodash.set';
import intel from 'intel';
import { existsSync, mkdirSync } from 'node:fs';
import path from 'node:path';
import { Engine } from '../lib/core/engine/index.js';
import {
findAndParseScripts,
allScriptCategories,
getScriptsForCategories
} from '../lib/support/browserScript.js';
import { configure } from '../lib/support/logging.js';
import { parseCommandLine } from '../lib/support/cli.js';
import { StorageManager } from '../lib/support/storageManager.js';
import { loadScript } from '../lib/support/engineUtils.js';
import { isAndroidConfigured } from '../lib/android/index.js';
const log = intel.getLogger('browsertime');
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const delay = ms => new Promise(response => setTimeout(response, ms));
async function parseUserScripts(scripts) {
if (!Array.isArray(scripts)) scripts = [scripts];
const results = {};
for (const script of scripts) {
const code = await findAndParseScripts(path.resolve(script), 'custom');
merge(results, code);
}
return results;
}
async function preWarmServer(urls, options) {
const preWarmOptions = {
browser: options.browser,
iterations: 1,
xvfb: options.xvfb,
android: isAndroidConfigured(options),
docker: options.docker,
headless: options.headless
};
const chromeDevice = get(options, 'chrome.android.deviceSerial');
const firefoxDevice = get(options, 'firefox.android.deviceSerial');
const safariIos = get(options, 'safari.ios');
const safariDeviceName = get(options, 'safari.deviceName');
const safariDeviceUDID = get(options, 'safari.deviceUDID ');
if (isAndroidConfigured(options) && options.browser === 'chrome') {
set(preWarmOptions, 'chrome.android.package', 'com.android.chrome');
}
if (chromeDevice) {
set(preWarmOptions, 'chrome.android.deviceSerial', chromeDevice);
} else if (firefoxDevice) {
set(preWarmOptions, 'firefox.android.deviceSerial', firefoxDevice);
}
if (safariIos) {
set(preWarmOptions, 'safari.ios', true);
if (safariDeviceName) {
set(preWarmOptions, 'safari.deviceName', safariDeviceName);
}
if (safariDeviceUDID) {
set(preWarmOptions, 'safari.deviceUDID', safariDeviceUDID);
}
}
let engine = new Engine(preWarmOptions);
await engine.start();
log.info('Start pre-testing/warming');
await engine.runMultiple(urls, {});
await engine.stop();
log.info('Pre-testing done, closed the browser.');
return delay(options.preWarmServerWaitTime || 5000);
}
async function run(urls, options) {
if (options.debug) {
log.info('Running Browsertime in debug mode.');
}
try {
if (!options.resultDir) {
let dir = 'browsertime-results';
if (!existsSync(dir)) {
mkdirSync(dir);
}
}
let engine = new Engine(options);
const scriptCategories = await allScriptCategories();
let scriptsByCategory = await getScriptsForCategories(scriptCategories);
if (options.script) {
const userScripts = await parseUserScripts(options.script);
scriptsByCategory = merge(scriptsByCategory, userScripts);
}
try {
if (options.preWarmServer) {
await preWarmServer(urls, options);
}
await engine.start();
const result = await engine.runMultiple(urls, scriptsByCategory);
let saveOperations = [];
// TODO setup by name
let firstUrl = urls[0];
// if the url is an array, it's of the form [filename, function]
if (Array.isArray(firstUrl)) {
firstUrl = firstUrl[0];
}
const storageManager = new StorageManager(firstUrl, options);
const harName = options.har ?? 'browsertime';
const jsonName = options.output ?? 'browsertime';
saveOperations.push(storageManager.writeJson(jsonName + '.json', result));
if (result.har) {
const useGzip = options.gzipHar === true;
saveOperations.push(
storageManager.writeJson(harName + '.har', result.har, useGzip)
);
}
await Promise.all(saveOperations);
const resultDirectory = path.relative(
process.cwd(),
storageManager.directory
);
// check for errors
// If we have set the exit code in scripts, respect that
if (process.exitCode === undefined) {
for (let eachResult of result) {
if (eachResult.markedAsFailure === 1) {
process.exitCode = 1;
}
for (let errors of eachResult.errors) {
if (errors.length > 0) {
process.exitCode = 1;
}
}
}
}
log.info(`Wrote data to ${resultDirectory}`);
} finally {
log.debug('Stopping Browsertime');
try {
await engine.stop();
log.debug('Stopped Browsertime');
} catch (error) {
log.error('Error stopping Browsertime!', error);
process.exitCode = 1;
}
}
if (options.enableProfileRun || options.enableVideoRun) {
log.info('Make one extra run to collect trace/video information');
options.iterations = 1;
if (options.enableProfileRun) {
if (options.browser === 'firefox') {
options.firefox.geckoProfiler = true;
} else if (options.browser === 'chrome') {
options.chrome.timeline = true;
options.cpu = true;
options.chrome.enableTraceScreenshots = true;
options.chrome.traceCategory = [
'disabled-by-default-v8.cpu_profiler'
];
}
}
if (options.enableVideoRun) {
if (options.video === true) {
log.error(
'You can only configure video run if you do not collect any video'
);
// This is a hack to not get an error
options.video = false;
options.visualMetrics = false;
} else {
options.video = true;
options.visualMetrics = true;
}
}
const traceEngine = new Engine(options);
await traceEngine.start();
await traceEngine.runMultiple(urls, scriptsByCategory);
await traceEngine.stop();
log.info('Extra run finished');
}
} catch (error) {
log.error('Error running browsertime', error);
process.exitCode = 1;
} finally {
process.exit();
}
}
let cliResult = parseCommandLine();
configure(cliResult.options);
/*
Each url can be:
- an url value
- an array of tests. In that case it's a mapping containing
theses values:
- test: an async function containing the test to run
- setUp: an async function for the preScript [optional]
- tearDown: an async function for the postScript [optional]
*/
const tests = [];
for (const url of cliResult.urls) {
// for each url, we try to load it as a script, that may contain
// export a single function or an object containing setUp/test/tearDown functions.
let testScript = await loadScript(url, cliResult.options);
// if the value is an url or a not a single function, we can return the original value
if (typeof testScript == 'string' || testScript instanceof AsyncFunction) {
tests.push(url);
continue;
}
if (testScript.setUp) {
if (!cliResult.options.preScript) {
cliResult.options.preScript = [];
}
cliResult.options.preScript.push(testScript.setUp);
}
if (testScript.tearDown) {
if (!cliResult.options.postScript) {
cliResult.options.postScript = [];
}
cliResult.options.postScript.push(testScript.tearDown);
}
// here, url is the filename containing the script, and test the callable.
tests.push([url, testScript.test]);
}
await run(tests, cliResult.options);