browsertime
Version:
Get performance metrics from your web page using Browsertime.
202 lines (181 loc) • 6 kB
JavaScript
import { getLogger } from '@sitespeed.io/log';
import path from 'node:path';
const { join } = path;
import { execa } from 'execa';
import { existsSync, renameSync } from 'node:fs';
import { isAndroidConfigured } from '../../../android/index.js';
// const delay = ms => new Promise(res => setTimeout(res, ms));
/**
* Manages the collection of perfetto traces on Android.
*
* @class
* @hideconstructor
*/
const log = getLogger('browsertime.command.simpleperf');
const defaultRecordOptions =
'--call-graph fp --duration 240 -f 1000 --trace-offcpu -e cpu-clock';
/**
* Timeout a promise after ms. Use promise.race to compete
* about the timeout and the promise.
* @param {promise} promise - the promise to wait for
* @param {int} ms - how long in ms to wait for the promise to fininsh
* @param {string} errorMessage - the error message in the Error if we timeouts
*/
async function timeout(promise, ms, errorMessage) {
let timer;
return Promise.race([
new Promise((resolve, reject) => {
timer = setTimeout(reject, ms, new Error(errorMessage));
return timer;
}),
promise.then(value => {
clearTimeout(timer);
return value;
})
]);
}
export class SimplePerfProfiler {
constructor(browser, index, storageManager, options) {
/**
* @private
*/
this.browser = browser;
/**
* @private
*/
this.storageManager = storageManager;
/**
* @private
*/
this.options = options;
/**
* @private
*/
this.index = index;
/**
* @private
*/
this.running = false;
}
/**
* Start Simpleperf profiling.
*
* @async
* @returns {Promise<void>} A promise that resolves when simpleperf has started profiling.
* @throws {Error} Throws an error if app_profiler.py fails to execute.
*/
async start(
profilerOptions = [],
recordOptions = defaultRecordOptions,
dirName = 'simpleperf'
) {
if (!isAndroidConfigured(this.options)) {
throw new Error('Simpleperf profiling is only available on Android.');
}
log.info('Starting simpleperf profiler.');
// Create empty subdirectory for simpleperf data.
let dirname = `${dirName}-${this.index}`;
let counter = 0;
this.profilerOptions = profilerOptions;
while (true) {
log.info(`Checking if ${dirname} exists...`);
if (existsSync(join(this.storageManager.directory, dirname))) {
dirname = `${dirName}-${this.index}.${counter}`;
counter++;
log.info(`Directory already exists.`);
} else {
this.dataDir = await this.storageManager.createSubDataDir(dirname);
log.info(`Creating subdir ${this.dataDir}.`);
break;
}
}
// Execute simpleperf.
const packageName =
this.options.browser === 'firefox'
? this.options.firefox?.android?.package
: this.options.chrome?.android?.package;
let simpleperfPath = this.options.androidSimpleperf;
let cmd = join(simpleperfPath, 'app_profiler.py');
let args = [
...profilerOptions,
'-p',
packageName,
'-r',
recordOptions,
'--log',
'debug',
'-o',
join(this.dataDir, 'perf.data')
];
this.simpleperfProcess = execa(cmd, args);
// Waiting for simpleperf to start.
let simpleperfPromise = new Promise((resolve, reject) => {
let stderrStream = this.simpleperfProcess.stderr;
stderrStream.on('data', data => {
let dataStr = data.toString();
log.info(dataStr);
if (/command 'record' starts running/.test(dataStr)) {
this.running = true;
stderrStream.removeAllListeners('data');
return resolve();
}
if (/Failed to record profiling data./.test(dataStr)) {
this.running = false;
log.info(`Error starting simpleperf: ${dataStr}`);
throw new Error('Simpleperf failed to start.');
}
});
stderrStream.once('error', reject);
});
// Set a 30s timeout for starting simpleperf.
return timeout(simpleperfPromise, 30_000, 'Simpleperf timed out.');
}
/**
* Stop Simpleperf profiling.
*
* @async
* @returns {Promise<void>} A promise that resolves when simpleperf has stopped profiling
* and collected profile data.
* @throws {Error} Throws an error if app_profiler.py fails to execute.
*/
async stop() {
if (!isAndroidConfigured(this.options)) {
throw new Error('Simpleperf profiling is only available on Android.');
}
if (!this.running) {
throw new Error('Simpleperf profiling was not started.');
}
log.info('Stop simpleperf profiler.');
this.simpleperfProcess.kill('SIGINT');
// Return when "profiling is finished." is found, or an error.
return new Promise((resolve, reject) => {
let stderrStream = this.simpleperfProcess.stderr;
log.info('Reading stderr.');
stderrStream.on('data', data => {
const dataStr = data.toString();
log.info(dataStr);
// Resolve immediately if -nb or --skip_collect_binaries is passed
// into the app_profiler options as a binary cache will not be produced.
if (
this.profilerOptions.includes('-nb') ||
this.profilerOptions.includes('--skip_collect_binaries')
) {
stderrStream.removeAllListeners('data');
return resolve();
}
if (/profiling is finished./.test(dataStr)) {
stderrStream.removeAllListeners('data');
// There is no way to specify the output of binary_cache,
// so manually move it (if it exists) into the data directory.
if (existsSync('binary_cache')) {
renameSync('binary_cache', join(this.dataDir, 'binary_cache'));
} else {
log.info('binary_cache does not exist.');
}
return resolve();
}
});
stderrStream.once('error', reject);
});
}
}