summaryrefslogtreecommitdiff
path: root/preact/benches/scripts/deopts.js
diff options
context:
space:
mode:
Diffstat (limited to 'preact/benches/scripts/deopts.js')
-rw-r--r--preact/benches/scripts/deopts.js253
1 files changed, 253 insertions, 0 deletions
diff --git a/preact/benches/scripts/deopts.js b/preact/benches/scripts/deopts.js
new file mode 100644
index 0000000..990a6cc
--- /dev/null
+++ b/preact/benches/scripts/deopts.js
@@ -0,0 +1,253 @@
+import * as path from 'path';
+import { mkdir } from 'fs/promises';
+import { spawn } from 'child_process';
+import { Transform } from 'stream';
+import escapeRe from 'escape-string-regexp';
+import stripAnsi from 'strip-ansi';
+import { pool } from '@kristoferbaxter/async';
+import {
+ globSrc,
+ benchesRoot,
+ getPkgBinPath,
+ resultsPath,
+ IS_CI
+} from './utils.js';
+import { generateConfig } from './config.js';
+import { defaultBenchOptions } from './bench.js';
+
+export const defaultDeoptsOptions = {
+ framework: 'preact-local',
+ timeout: 5,
+ open: IS_CI ? false : true
+};
+
+const getResultDir = (benchmark, framework) =>
+ resultsPath('v8-deopt-viewer', benchmark, framework);
+
+/**
+ * @param {string} pkgName
+ * @param {string[]} args
+ * @param {"pipe" | "inherit"} [stdio]
+ * @returns {Promise<import('child_process').ChildProcess>}
+ */
+async function runPackage(pkgName, args, stdio) {
+ const binPath = await getPkgBinPath(pkgName);
+ args.unshift(binPath);
+
+ return spawn(process.execPath, args, { stdio });
+}
+
+/**
+ * @param {import('child_process').ChildProcess} childProcess
+ */
+async function onExit(childProcess) {
+ return new Promise((resolve, reject) => {
+ childProcess.once('exit', (code, signal) => {
+ if (code === 0 || signal == 'SIGINT') {
+ resolve();
+ } else {
+ reject(new Error('Exit with error code: ' + code));
+ }
+ });
+
+ childProcess.once('error', err => {
+ reject(err);
+ });
+ });
+}
+
+/**
+ * @typedef {{ benchName: string; framework: string; url: string; }} TachURL
+ * @param {import('child_process').ChildProcess} tachProcess
+ * @param {import('./config').ConfigData} tachConfig
+ * @param {number} timeoutMs
+ * @returns {Promise<TachURL[]>}
+ */
+async function getTachometerURLs(tachProcess, tachConfig, timeoutMs = 60e3) {
+ return new Promise(async (resolve, reject) => {
+ let timeout;
+ if (timeoutMs > 0) {
+ timeout = setTimeout(() => {
+ reject(
+ new Error(
+ 'Timed out waiting for Tachometer to get set up. Did it output a URL?'
+ )
+ );
+ }, timeoutMs);
+ }
+
+ // Look for lines like:
+ // many_updates [@preact]
+ // http://127.0.0.1:56536/src/many_updates.html
+ const benchesToSearch = tachConfig.config.benchmarks.map(bench => ({
+ benchName: bench.name,
+ framework: bench.packageVersions.label,
+ regex: new RegExp(
+ escapeRe(`${bench.name} [@${bench.packageVersions.label}]`) +
+ `\\s+(http:\\/\\/.*)`,
+ 'im'
+ ),
+ url: null
+ }));
+
+ /** @type {TachURL[]} */
+ const results = [];
+ let output = '';
+ tachProcess.stdout.on('data', function onStdOutChunk(chunk) {
+ output += stripAnsi(chunk.toString('utf8'));
+
+ for (let bench of benchesToSearch) {
+ if (bench.url) {
+ continue;
+ }
+
+ let match = output.match(bench.regex);
+ if (match) {
+ bench.url = match[1];
+ results.push(bench);
+ }
+ }
+
+ if (results.length == benchesToSearch.length) {
+ // All URLs found, removeEventListener
+ tachProcess.off('data', onStdOutChunk);
+
+ clearTimeout(timeout);
+ resolve(results);
+ }
+ });
+ });
+}
+
+function createPrefixTransform(prefix) {
+ return new Transform({
+ transform(chunk, encoding, callback) {
+ try {
+ // @ts-ignore
+ chunk = encoding == 'buffer' ? chunk.toString() : chunk;
+ const lines = chunk.split('\n');
+
+ for (let line of lines) {
+ if (line) {
+ line = `[${prefix}] ${line}`;
+ this.push(line + '\n');
+ }
+ }
+
+ callback();
+ } catch (error) {
+ return callback(error);
+ }
+ }
+ });
+}
+
+/**
+ * @param {TachURL} tachURL
+ * @param {DeoptOptions} options
+ */
+async function runV8DeoptViewer(tachURL, options) {
+ const deoptOutputDir = getResultDir(tachURL.benchName, tachURL.framework);
+ await mkdir(deoptOutputDir, { recursive: true });
+
+ const deoptArgs = [
+ tachURL.url,
+ '-o',
+ deoptOutputDir,
+ '-t',
+ (options.timeout * 1000).toString()
+ ];
+
+ if (options.open) {
+ deoptArgs.push('--open');
+ }
+
+ const deoptProcess = await runPackage('v8-deopt-viewer', deoptArgs);
+ deoptProcess.stdout
+ .pipe(createPrefixTransform(tachURL.framework))
+ .pipe(process.stdout);
+ deoptProcess.stderr
+ .pipe(createPrefixTransform(tachURL.framework))
+ .pipe(process.stderr);
+
+ await onExit(deoptProcess);
+}
+
+/**
+ * @param {string} benchGlob
+ * @param {DeoptOptions} options
+ */
+export async function runDeopts(benchGlob, options) {
+ // TODO:
+ // * Handle multiple benchmarks
+
+ const frameworks = options.framework;
+ if (!benchGlob) {
+ benchGlob = 'many_updates.html';
+ }
+
+ const benchesToRun = await globSrc(benchGlob);
+ if (benchesToRun.length > 1) {
+ console.error('Matched multiple benchmarks. Only running the first one.');
+ }
+
+ const benchPath = benchesRoot('src', benchesToRun[0]);
+ const tachConfig = await generateConfig(benchPath, {
+ ...defaultBenchOptions,
+ ...defaultDeoptsOptions,
+ framework: frameworks
+ });
+
+ console.log('Benchmarks running:', benchPath);
+ console.log('Frameworks running:', frameworks);
+
+ /** @type {Promise<void>} */
+ let onTachExit;
+ /** @type {import('child_process').ChildProcess} */
+ let tachProcess;
+ try {
+ // Run tachometer in manual mode with generated config
+ const tachArgs = ['--config', tachConfig.configPath, '--manual'];
+ tachProcess = await runPackage('tachometer', tachArgs);
+ tachProcess.stdout.pipe(process.stdout);
+ tachProcess.stderr.pipe(process.stderr);
+ onTachExit = onExit(tachProcess);
+
+ // Parse URL from tachometer stdout
+ const tachURLs = await getTachometerURLs(tachProcess, tachConfig);
+
+ // Run v8-deopt-viewer against tachometer URL
+ console.log();
+ await pool(tachURLs, tachURL =>
+ runV8DeoptViewer(tachURL, {
+ ...options,
+ open: options.open && tachURLs.length == 1
+ })
+ );
+
+ if (tachURLs.length > 1) {
+ const rootResultDir = getResultDir('', '');
+ console.log(`\nOpen your browser to ${rootResultDir} to view results.`);
+
+ if (options.open) {
+ // TODO: Figure out how to open a directory in the user's default browser
+ }
+ }
+ } finally {
+ if (tachProcess) {
+ tachProcess.kill('SIGINT');
+
+ // Log a message is Tachometer takes a while to close
+ let logMsg = () => console.log('Waiting for Tachometer to exit...');
+ let t = setTimeout(logMsg, 2e3);
+
+ try {
+ await onTachExit;
+ } catch (error) {
+ console.error('Error waiting for Tachometer to exit:', error);
+ } finally {
+ clearTimeout(t);
+ }
+ }
+ }
+}