Skip to content

Commit

Permalink
feat: parallelize benchmarks build (#170)
Browse files Browse the repository at this point in the history
* feat: parallelize benchmarks build

* fix: types

* fix: more types

* fix: lint
  • Loading branch information
jodarove authored and Diego Ferreiro Val committed Jul 3, 2019
1 parent 45d96cc commit 4b679aa
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 96 deletions.
4 changes: 3 additions & 1 deletion packages/@best/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"types": "build/index.d.ts",
"dependencies": {
"@best/runtime": "4.0.0",
"@best/utils": "4.0.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.2",
"rollup": "~1.15.5",
"mkdirp": "~0.5.1"
"mkdirp": "~0.5.1",
"node-worker-farm": "~1.3.1"
}
}
2 changes: 1 addition & 1 deletion packages/@best/builder/src/__tests__/best-build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import os from 'os';
import fs from 'fs';
import path from 'path';

import { buildBenchmark } from '../index';
import { buildBenchmark } from '../build-benchmark';

const GLOBAL_CONFIG = {
gitInfo: {
Expand Down
39 changes: 39 additions & 0 deletions packages/@best/builder/src/build-benchmark-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { buildBenchmark } from "./build-benchmark";

const send = process.send && process.send.bind(process);

if (!send) {
throw new Error('This module must be used as a forked child');
}

const messagerAdapter = {
onBenchmarkBuildStart(benchmarkPath: string) {
send({
type: 'messager.onBenchmarkBuildStart',
benchmarkPath
});
},
log(message: string) {
send({
type: 'messager.log',
message,
});
},
onBenchmarkBuildEnd(benchmarkPath: string) {
send({
type: 'messager.onBenchmarkBuildEnd',
benchmarkPath
});
}
};

module.exports = async function (input: any, callback: Function) {
const result = await buildBenchmark(
input.benchmark,
input.projectConfig,
input.globalConfig,
messagerAdapter
);

callback(null, result);
};
92 changes: 92 additions & 0 deletions packages/@best/builder/src/build-benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'fs';
import { rollup, ModuleFormat } from 'rollup';
import path from 'path';
import crypto from 'crypto';
import mkdirp from "mkdirp";
import benchmarkRollup from './rollup-plugin-benchmark-import';
import generateHtml from './html-templating';
import { FrozenGlobalConfig, FrozenProjectConfig, ProjectConfigPlugin, BuildConfig } from '@best/types';

const BASE_ROLLUP_OUTPUT = { format: 'iife' as ModuleFormat };
const ROLLUP_CACHE = new Map();

function md5(data: string) {
return crypto.createHash('md5').update(data).digest('hex');
}

// Handles default exports for both ES5 and ES6 syntax
function req(id: string) {
const r = require(id);
return r.default || r;
}

function addResolverPlugins(plugins: ProjectConfigPlugin[]): any[] {
if (!plugins) {
return [];
}

return plugins.map((plugin: ProjectConfigPlugin) => {
if (typeof plugin === 'string') {
return req(plugin)();
} else if (Array.isArray(plugin)) {
return req(plugin[0])(plugin[1]);
} else {
throw new Error('Invalid plugin config');
}
});
}

interface BuildOutputMessager {
onBenchmarkBuildStart(benchmarkPath: string): void;
onBenchmarkBuildEnd(benchmarkPath: string): void;
log(message: string): void;
}

export async function buildBenchmark(entry: string, projectConfig: FrozenProjectConfig, globalConfig: FrozenGlobalConfig, buildLogStream: BuildOutputMessager): Promise<BuildConfig> {
buildLogStream.onBenchmarkBuildStart(entry);

const { gitInfo: { lastCommit: { hash: gitHash }, localChanges } } = globalConfig;
const { projectName, benchmarkOutput, rootDir } = projectConfig;
const ext = path.extname(entry);
const benchmarkName = path.basename(entry, ext);
const benchmarkJSFileName = benchmarkName + ext;
const benchmarkProjectFolder = path.join(benchmarkOutput, projectName);

const rollupInputOpts = {
input: entry,
plugins: [benchmarkRollup(), ...addResolverPlugins(projectConfig.plugins)],
cache: ROLLUP_CACHE.get(projectName)
};

buildLogStream.log('Bundling benchmark files...');
const bundle = await rollup(rollupInputOpts);
ROLLUP_CACHE.set(projectName, bundle.cache);

buildLogStream.log('Generating benchmark artifacts...');
const rollupOutputOpts = { ...BASE_ROLLUP_OUTPUT };
const { output } = await bundle.generate(rollupOutputOpts);
const benchmarkSource = output[0].code; // We don't do code splitting so the first one will be the one we want

// Benchmark artifacts vars
const benchmarkSignature = md5(benchmarkSource);
const benchmarkSnapshotName = localChanges ? `${benchmarkName}_local_${benchmarkSignature.slice(0, 10)}` : `${benchmarkName}_${gitHash}`;
const benchmarkFolder = path.join(benchmarkProjectFolder, benchmarkSnapshotName);
const benchmarkArtifactsFolder = path.join(benchmarkFolder, 'artifacts');
const benchmarkEntry = path.join(benchmarkArtifactsFolder, `${benchmarkName}.html`);
const htmlTemplate = generateHtml({ benchmarkName, benchmarkJs: `./${benchmarkJSFileName}` });

mkdirp.sync(benchmarkArtifactsFolder);
fs.writeFileSync(benchmarkEntry, htmlTemplate, 'utf-8');
fs.writeFileSync(path.join(benchmarkArtifactsFolder, benchmarkJSFileName), benchmarkSource, 'utf-8');

buildLogStream.onBenchmarkBuildEnd(entry);

return {
benchmarkName,
benchmarkFolder: path.relative(rootDir, benchmarkFolder),
benchmarkEntry: path.relative(rootDir, benchmarkEntry),
benchmarkSignature,
projectConfig,
globalConfig,
};
}
138 changes: 49 additions & 89 deletions packages/@best/builder/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,56 @@
import fs from 'fs';
import { rollup, ModuleFormat } from 'rollup';
import path from 'path';
import crypto from 'crypto';
import mkdirp from "mkdirp";
import benchmarkRollup from './rollup-plugin-benchmark-import';
import generateHtml from './html-templating';
import { FrozenGlobalConfig, FrozenProjectConfig, ProjectConfigPlugin, BuildConfig } from '@best/types';
import { BuildOutputStream } from "@best/console-stream"
import { FrozenGlobalConfig, FrozenProjectConfig, BuildConfig } from '@best/types';
import { BuildOutputStream } from "@best/console-stream";
import { isCI } from '@best/utils';
import workerFarm from "worker-farm";

const BASE_ROLLUP_OUTPUT = { format: 'iife' as ModuleFormat };
const ROLLUP_CACHE = new Map();
const DEFAULT_FARM_OPTS = {
maxConcurrentWorkers: isCI ? 2 : require('os').cpus().length,
maxConcurrentCallsPerWorker: 1,
};

function md5(data: string) {
return crypto.createHash('md5').update(data).digest('hex');
}

// Handles default exports for both ES5 and ES6 syntax
function req(id: string) {
const r = require(id);
return r.default || r;
}
interface ChildMessage { type: string, benchmarkPath: string, message: string }

function addResolverPlugins(plugins: ProjectConfigPlugin[]): any[] {
if (!plugins) {
return [];
}

return plugins.map((plugin: ProjectConfigPlugin) => {
if (typeof plugin === 'string') {
return req(plugin)();
} else if (Array.isArray(plugin)) {
return req(plugin[0])(plugin[1]);
} else {
throw new Error('Invalid plugin config');
export async function buildBenchmarks(benchmarks: string[], projectConfig: FrozenProjectConfig, globalConfig: FrozenGlobalConfig, buildLogStream: BuildOutputStream): Promise<BuildConfig[]> {
const opts = {
...DEFAULT_FARM_OPTS,
onChild: (child: NodeJS.Process) => {
child.on("message", (message: ChildMessage) => {
if (message.type === 'messager.onBenchmarkBuildStart') {
buildLogStream.onBenchmarkBuildStart(message.benchmarkPath);
} else if (message.type === 'messager.log') {
buildLogStream.log(message.message);
} else if (message.type === 'messager.onBenchmarkBuildEnd') {
buildLogStream.onBenchmarkBuildEnd(message.benchmarkPath);
}
})
}
});
}

export async function buildBenchmark(entry: string, projectConfig: FrozenProjectConfig, globalConfig: FrozenGlobalConfig, buildLogStream: BuildOutputStream): Promise<BuildConfig> {
buildLogStream.onBenchmarkBuildStart(entry);

const { gitInfo: { lastCommit: { hash: gitHash }, localChanges } } = globalConfig;
const { projectName, benchmarkOutput, rootDir } = projectConfig;
const ext = path.extname(entry);
const benchmarkName = path.basename(entry, ext);
const benchmarkJSFileName = benchmarkName + ext;
const benchmarkProjectFolder = path.join(benchmarkOutput, projectName);

const rollupInputOpts = {
input: entry,
plugins: [benchmarkRollup(), ...addResolverPlugins(projectConfig.plugins)],
cache: ROLLUP_CACHE.get(projectName)
};

buildLogStream.log('Bundling benchmark files...');
const bundle = await rollup(rollupInputOpts);
ROLLUP_CACHE.set(projectName, bundle.cache);

buildLogStream.log('Generating benchmark artifacts...');
const rollupOutputOpts = { ...BASE_ROLLUP_OUTPUT };
const { output } = await bundle.generate(rollupOutputOpts);
const benchmarkSource = output[0].code; // We don't do code splitting so the first one will be the one we want

// Benchmark artifacts vars
const benchmarkSignature = md5(benchmarkSource);
const benchmarkSnapshotName = localChanges ? `${benchmarkName}_local_${benchmarkSignature.slice(0, 10)}` : `${benchmarkName}_${gitHash}`;
const benchmarkFolder = path.join(benchmarkProjectFolder, benchmarkSnapshotName);
const benchmarkArtifactsFolder = path.join(benchmarkFolder, 'artifacts');
const benchmarkEntry = path.join(benchmarkArtifactsFolder, `${benchmarkName}.html`);
const htmlTemplate = generateHtml({ benchmarkName, benchmarkJs: `./${benchmarkJSFileName}` });

mkdirp.sync(benchmarkArtifactsFolder);
fs.writeFileSync(benchmarkEntry, htmlTemplate, 'utf-8');
fs.writeFileSync(path.join(benchmarkArtifactsFolder, benchmarkJSFileName), benchmarkSource, 'utf-8');

buildLogStream.onBenchmarkBuildEnd(entry);

return {
benchmarkName,
benchmarkFolder: path.relative(rootDir, benchmarkFolder),
benchmarkEntry: path.relative(rootDir, benchmarkEntry),
benchmarkSignature,
projectConfig,
globalConfig,
};
}

export async function buildBenchmarks(benchmarks: string[], projectConfig: FrozenProjectConfig, globalConfig: FrozenGlobalConfig, buildLogStream: BuildOutputStream): Promise<BuildConfig[]> {
const benchBuild = [];
for (const benchmark of benchmarks) {
const build = await buildBenchmark(benchmark, projectConfig, globalConfig, buildLogStream);
benchBuild.push(build);
}
return benchBuild;
const workers = workerFarm(opts, require.resolve('./build-benchmark-worker'));
const jobs = benchmarks.length;
let jobsCompleted = 0;
const benchBuild: BuildConfig[] = [];

return new Promise((resolve, reject) => {
benchmarks.forEach(benchmark => {
const buildInfo = {
benchmark,
projectConfig,
globalConfig
};

workers(buildInfo, (err: any, result: BuildConfig) => {
if (err) {
return reject(err);
}

benchBuild.push(result);

if (++jobsCompleted === jobs) {
workerFarm.end(workers);
resolve(benchBuild);
}
});
});
});
}
2 changes: 1 addition & 1 deletion packages/@best/builder/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "../../../tsconfig.settings.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build",
"outDir": "build"
},
"references": [
{ "path": "../runtime" },
Expand Down
3 changes: 1 addition & 2 deletions packages/@best/cli/src/run_best.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ async function getBenchmarkTests(projectConfigs: FrozenProjectConfig[], globalCo

async function buildBundleBenchmarks(benchmarksTests: { config: FrozenProjectConfig; matches: string[] }[], globalConfig: FrozenGlobalConfig, messager: BuildOutputStream) {
const benchmarkBuilds: BuildConfig[] = [];
// @dval: We don't parallelize here for now since this wouldn't give us much,
// Unless we do proper spawning on threads
// We wait for each project to run before starting the next batch
for (const benchmarkTest of benchmarksTests) {
const { matches, config } = benchmarkTest;
const result = await buildBenchmarks(matches, config, globalConfig, messager);
Expand Down
12 changes: 10 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5162,7 +5162,7 @@ err-code@^1.0.0:
resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=

errno@^0.1.3, errno@~0.1.7:
"errno@>=0.1.1 <0.2.0-0", errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
Expand Down Expand Up @@ -9081,6 +9081,14 @@ node-releases@^1.1.3:
dependencies:
semver "^5.3.0"

node-worker-farm@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-worker-farm/-/node-worker-farm-1.3.1.tgz#6e66ffcfc0cd1927ee0319260ea822199d7054ff"
integrity sha1-bmb/z8DNGSfuAxkmDqgiGZ1wVP8=
dependencies:
errno ">=0.1.1 <0.2.0-0"
xtend ">=4.0.0 <4.1.0-0"

nodemon@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"
Expand Down Expand Up @@ -13186,7 +13194,7 @@ xmlhttprequest-ssl@~1.5.4:
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=

xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
Expand Down

0 comments on commit 4b679aa

Please sign in to comment.