-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: parallelize benchmarks build (#170)
* feat: parallelize benchmarks build * fix: types * fix: more types * fix: lint
- Loading branch information
Showing
8 changed files
with
196 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters