mirror of https://github.com/webpack/webpack.git
1063 lines
25 KiB
JavaScript
1063 lines
25 KiB
JavaScript
import { constants, writeFile } from "fs";
|
|
import fs from "fs/promises";
|
|
import { Session } from "inspector";
|
|
import path from "path";
|
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
import {
|
|
InstrumentHooks,
|
|
getCodspeedRunnerMode,
|
|
getGitDir,
|
|
getV8Flags,
|
|
mongoMeasurement,
|
|
optimizeFunction,
|
|
setupCore,
|
|
teardownCore
|
|
} from "@codspeed/core";
|
|
import { simpleGit } from "simple-git";
|
|
import { Bench, hrtimeNow } from "tinybench";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const rootPath = path.join(__dirname, "..");
|
|
const git = simpleGit(rootPath);
|
|
|
|
const REV_LIST_REGEXP = /^([a-f0-9]+)\s*([a-f0-9]+)\s*([a-f0-9]+)?\s*$/;
|
|
|
|
const checkV8Flags = () => {
|
|
const requiredFlags = getV8Flags();
|
|
const actualFlags = process.execArgv;
|
|
const missingFlags = requiredFlags.filter(
|
|
(flag) => !actualFlags.includes(flag)
|
|
);
|
|
if (missingFlags.length > 0) {
|
|
console.warn(`Missing required flags: ${missingFlags.join(", ")}`);
|
|
}
|
|
};
|
|
|
|
checkV8Flags();
|
|
|
|
const LAST_COMMIT = typeof process.env.LAST_COMMIT !== "undefined";
|
|
const GENERATE_PROFILE = typeof process.env.PROFILE !== "undefined";
|
|
|
|
/**
|
|
* @param {(string | undefined)[]} revList rev list
|
|
* @returns {Promise<string>} head
|
|
*/
|
|
async function getHead(revList) {
|
|
if (typeof process.env.HEAD !== "undefined") {
|
|
return process.env.HEAD;
|
|
}
|
|
|
|
// On CI we take the latest commit `merge commit` as a head
|
|
if (revList[3]) {
|
|
return revList[3];
|
|
}
|
|
|
|
// Otherwise we take the latest commit
|
|
return revList[1];
|
|
}
|
|
|
|
/**
|
|
* @param {string} head head
|
|
* @param {(string | undefined)[]} revList rev list
|
|
* @returns {Promise<string>} base
|
|
*/
|
|
async function getBase(head, revList) {
|
|
if (typeof process.env.BASE !== "undefined") {
|
|
return process.env.BASE;
|
|
}
|
|
|
|
if (revList[3]) {
|
|
return revList[2];
|
|
}
|
|
|
|
const branchName = await git.raw(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
|
|
if (branchName.trim() !== "main") {
|
|
const resultParents = await git.raw([
|
|
"rev-list",
|
|
"--parents",
|
|
"-n",
|
|
"1",
|
|
"main"
|
|
]);
|
|
|
|
const revList = REV_LIST_REGEXP.exec(resultParents);
|
|
|
|
if (!revList[1]) {
|
|
throw new Error("No parent commit found");
|
|
}
|
|
|
|
if (head === revList[1]) {
|
|
return revList[2];
|
|
}
|
|
|
|
return revList[1];
|
|
}
|
|
|
|
return revList[2];
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<{name: string, rev: string}[]>} baseline revs
|
|
*/
|
|
async function getBaselineRevs() {
|
|
if (LAST_COMMIT) {
|
|
return [
|
|
{
|
|
name: "HEAD"
|
|
}
|
|
];
|
|
}
|
|
|
|
const resultParents = await git.raw([
|
|
"rev-list",
|
|
"--parents",
|
|
"-n",
|
|
"1",
|
|
"HEAD"
|
|
]);
|
|
const revList = REV_LIST_REGEXP.exec(resultParents);
|
|
|
|
if (!revList) throw new Error("Invalid result from git rev-list");
|
|
|
|
const head = await getHead(revList);
|
|
const base = await getBase(head, revList);
|
|
|
|
if (!head || !base) {
|
|
throw new Error("No baseline found");
|
|
}
|
|
|
|
return [
|
|
{
|
|
name: "HEAD",
|
|
rev: head
|
|
},
|
|
{
|
|
name: "BASE",
|
|
rev: base
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {number} n number of runs
|
|
* @returns {number} distribution
|
|
*/
|
|
function tDistribution(n) {
|
|
// two-sided, 90%
|
|
// https://en.wikipedia.org/wiki/Student%27s_t-distribution
|
|
if (n <= 30) {
|
|
// 1 2 ...
|
|
const data = [
|
|
6.314, 2.92, 2.353, 2.132, 2.015, 1.943, 1.895, 1.86, 1.833, 1.812, 1.796,
|
|
1.782, 1.771, 1.761, 1.753, 1.746, 1.74, 1.734, 1.729, 1.725, 1.721,
|
|
1.717, 1.714, 1.711, 1.708, 1.706, 1.703, 1.701, 1.699, 1.697
|
|
];
|
|
return data[n - 1];
|
|
} else if (n <= 120) {
|
|
// 30 40 50 60 70 80 90 100 110 120
|
|
const data = [
|
|
1.697, 1.684, 1.676, 1.671, 1.667, 1.664, 1.662, 1.66, 1.659, 1.658
|
|
];
|
|
const a = data[Math.floor(n / 10) - 3];
|
|
const b = data[Math.ceil(n / 10) - 3];
|
|
const f = n / 10 - Math.floor(n / 10);
|
|
|
|
return a * (1 - f) + b * f;
|
|
}
|
|
|
|
return 1.645;
|
|
}
|
|
|
|
const output = path.join(__dirname, "js");
|
|
const baselinesPath = path.join(output, "benchmark-baselines");
|
|
|
|
const baselineRevisions = await getBaselineRevs();
|
|
|
|
try {
|
|
await fs.mkdir(baselinesPath, { recursive: true });
|
|
} catch (_err) {} // eslint-disable-line no-empty
|
|
|
|
const baselines = [];
|
|
|
|
for (const baselineInfo of baselineRevisions) {
|
|
/**
|
|
* @returns {void}
|
|
*/
|
|
function addBaseline() {
|
|
baselines.push({
|
|
name: baselineInfo.name,
|
|
rev: baselineRevision,
|
|
webpack: async () => {
|
|
const webpack = (
|
|
await import(
|
|
pathToFileURL(path.resolve(baselinePath, "./lib/index.js"))
|
|
)
|
|
).default;
|
|
|
|
return webpack;
|
|
}
|
|
});
|
|
}
|
|
|
|
const baselineRevision = baselineInfo.rev;
|
|
const baselinePath =
|
|
baselineRevision === undefined
|
|
? path.resolve(__dirname, "../")
|
|
: path.resolve(baselinesPath, baselineRevision);
|
|
|
|
try {
|
|
await fs.access(path.resolve(baselinePath, ".git"), constants.R_OK);
|
|
} catch (_err) {
|
|
try {
|
|
await fs.mkdir(baselinePath);
|
|
} catch (_err) {} // eslint-disable-line no-empty
|
|
|
|
const gitIndex = path.resolve(rootPath, ".git/index");
|
|
const index = await fs.readFile(gitIndex);
|
|
const prevHead = await git.raw(["rev-list", "-n", "1", "HEAD"]);
|
|
|
|
await simpleGit(baselinePath).raw([
|
|
"--git-dir",
|
|
path.join(rootPath, ".git"),
|
|
"reset",
|
|
"--hard",
|
|
baselineRevision
|
|
]);
|
|
|
|
await git.raw(["reset", "--soft", prevHead.split("\n")[0]]);
|
|
await fs.writeFile(gitIndex, index);
|
|
} finally {
|
|
addBaseline();
|
|
}
|
|
}
|
|
|
|
const baseOutputPath = path.join(__dirname, "js", "benchmark");
|
|
|
|
function buildConfiguration(
|
|
test,
|
|
baseline,
|
|
realConfig,
|
|
scenario,
|
|
testDirectory
|
|
) {
|
|
const { watch, ...rest } = scenario;
|
|
const config = structuredClone({ ...realConfig, ...rest });
|
|
|
|
config.entry = path.resolve(
|
|
testDirectory,
|
|
config.entry
|
|
? /\.(c|m)?js$/.test(config.entry)
|
|
? config.entry
|
|
: `${config.entry}.js`
|
|
: "./index.js"
|
|
);
|
|
config.devtool = config.devtool || false;
|
|
config.name = `${test}-${baseline.name}-${scenario.name}`;
|
|
config.context = testDirectory;
|
|
config.performance = false;
|
|
config.output = config.output || {};
|
|
config.output.path = path.join(
|
|
baseOutputPath,
|
|
test,
|
|
`scenario-${scenario.name}`,
|
|
`baseline-${baseline.name}`
|
|
);
|
|
config.plugins = config.plugins || [];
|
|
if (config.cache) {
|
|
config.cache.cacheDirectory = path.resolve(config.output.path, ".cache");
|
|
}
|
|
if (watch) {
|
|
config.cache = {
|
|
type: "memory",
|
|
maxGenerations: 1
|
|
};
|
|
}
|
|
return config;
|
|
}
|
|
|
|
// Filename sanitization
|
|
function sanitizeFilename(filename) {
|
|
// Replace invalid filesystem characters with underscores
|
|
return filename
|
|
.replace(/[<>:"/\\|?*]/g, "_")
|
|
.replace(/[\u0000-\u001F\u0080-\u009F]/g, "_")
|
|
.replace(/^\.+/, "_")
|
|
.replace(/\.+$/, "_")
|
|
.replace(/\s+/g, "_")
|
|
.slice(0, 200); // Limit filename length
|
|
}
|
|
|
|
async function withProfiling(name, fn) {
|
|
// Ensure the profiles directory exists
|
|
await fs.mkdir(path.join(baseOutputPath, "profiles"), { recursive: true });
|
|
|
|
const session = new Session();
|
|
session.connect();
|
|
|
|
// Enable and start profiling
|
|
await new Promise((resolve, reject) => {
|
|
session.post("Profiler.enable", (err) => {
|
|
if (err) return reject(err);
|
|
session.post("Profiler.start", (err) => {
|
|
if (err) return reject(err);
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Run the benchmarked function
|
|
// No need to `console.time`, it'll be included in the
|
|
// CPU Profile.
|
|
await fn();
|
|
|
|
// Stop profiling and get the CPU profile
|
|
const profile = await new Promise((resolve, reject) => {
|
|
session.post("Profiler.stop", (err, { profile }) => {
|
|
if (err) return reject(err);
|
|
resolve(profile);
|
|
});
|
|
});
|
|
|
|
session.disconnect();
|
|
|
|
const outputFile = `${sanitizeFilename(name)}-${Date.now()}.cpuprofile`;
|
|
|
|
await fs.writeFile(
|
|
path.join(baseOutputPath, "profiles", outputFile),
|
|
JSON.stringify(profile),
|
|
"utf8"
|
|
);
|
|
|
|
console.log(`CPU profile saved to ${outputFile}`);
|
|
}
|
|
|
|
function runWebpack(webpack, config) {
|
|
return new Promise((resolve, reject) => {
|
|
const compiler = webpack(config);
|
|
compiler.run((err, stats) => {
|
|
if (err) return reject(err);
|
|
if (stats && (stats.hasWarnings() || stats.hasErrors())) {
|
|
return reject(new Error(stats.toString()));
|
|
}
|
|
|
|
compiler.close((closeErr) => {
|
|
if (closeErr) return reject(closeErr);
|
|
if (stats) stats.toString(); // Force stats computation
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function runWatch(webpack, config, callback) {
|
|
const compiler = webpack(config);
|
|
return compiler.watch({}, callback);
|
|
}
|
|
|
|
const scenarios = [
|
|
{
|
|
name: "mode-development",
|
|
mode: "development"
|
|
},
|
|
{
|
|
name: "mode-development-rebuild",
|
|
mode: "development",
|
|
watch: true
|
|
},
|
|
{
|
|
name: "mode-production",
|
|
mode: "production"
|
|
}
|
|
];
|
|
|
|
function getStackTrace(belowFn) {
|
|
const oldLimit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = Infinity;
|
|
const dummyObject = {};
|
|
const v8Handler = Error.prepareStackTrace;
|
|
Error.prepareStackTrace = (dummyObject, v8StackTrace) => v8StackTrace;
|
|
Error.captureStackTrace(dummyObject, belowFn || getStackTrace);
|
|
const v8StackTrace = dummyObject.stack;
|
|
Error.prepareStackTrace = v8Handler;
|
|
Error.stackTraceLimit = oldLimit;
|
|
return v8StackTrace;
|
|
}
|
|
|
|
function getCallingFile() {
|
|
const stack = getStackTrace();
|
|
let callingFile = stack[2].getFileName(); // [here, withCodSpeed, actual caller]
|
|
const gitDir = getGitDir(callingFile);
|
|
if (gitDir === undefined) {
|
|
throw new Error("Could not find a git repository");
|
|
}
|
|
if (callingFile.startsWith("file://")) {
|
|
callingFile = fileURLToPath(callingFile);
|
|
}
|
|
return path.relative(gitDir, callingFile);
|
|
}
|
|
|
|
const taskUriMap = new WeakMap();
|
|
|
|
function getOrCreateUriMap(bench) {
|
|
let uriMap = taskUriMap.get(bench);
|
|
if (!uriMap) {
|
|
uriMap = new Map();
|
|
taskUriMap.set(bench, uriMap);
|
|
}
|
|
return uriMap;
|
|
}
|
|
|
|
function getTaskUri(bench, taskName, rootCallingFile) {
|
|
const uriMap = taskUriMap.get(bench);
|
|
return uriMap?.get(taskName) || `${rootCallingFile}::${taskName}`;
|
|
}
|
|
|
|
const withCodSpeed = async (/** @type {import("tinybench").Bench} */ bench) => {
|
|
const codspeedRunnerMode = getCodspeedRunnerMode();
|
|
|
|
if (codspeedRunnerMode === "disabled") {
|
|
return bench;
|
|
}
|
|
|
|
const rawAdd = bench.add;
|
|
const uriMap = getOrCreateUriMap(bench);
|
|
bench.add = (name, fn, opts) => {
|
|
const callingFile = getCallingFile();
|
|
let uri = callingFile;
|
|
if (bench.name !== undefined) {
|
|
uri += `::${bench.name}`;
|
|
}
|
|
uri += `::${name}`;
|
|
uriMap.set(name, uri);
|
|
return rawAdd.bind(bench)(name, fn, opts);
|
|
};
|
|
const rootCallingFile = getCallingFile();
|
|
|
|
if (codspeedRunnerMode === "instrumented") {
|
|
const setupBenchRun = () => {
|
|
setupCore();
|
|
console.log(
|
|
"[CodSpeed] running with @codspeed/tinybench (instrumented mode)"
|
|
);
|
|
};
|
|
const finalizeBenchRun = () => {
|
|
teardownCore();
|
|
console.log(`[CodSpeed] Done running ${bench.tasks.length} benches.`);
|
|
return bench.tasks;
|
|
};
|
|
|
|
const wrapFunctionWithFrame = (fn, isAsync) => {
|
|
if (isAsync) {
|
|
return async function __codspeed_root_frame__() {
|
|
await fn();
|
|
};
|
|
}
|
|
|
|
return function __codspeed_root_frame__() {
|
|
fn();
|
|
};
|
|
};
|
|
|
|
const logTaskCompletion = (uri, status) => {
|
|
console.log(`[CodSpeed] ${status} ${uri}`);
|
|
};
|
|
|
|
const taskCompletionMessage = () =>
|
|
InstrumentHooks.isInstrumented() ? "Measured" : "Checked";
|
|
|
|
const iterationAsync = async (task) => {
|
|
try {
|
|
await task.fnOpts.beforeEach?.call(task, "run");
|
|
const start = bench.opts.now();
|
|
await task.fn();
|
|
const end = bench.opts.now() - start || 0;
|
|
await task.fnOpts.afterEach?.call(this, "run");
|
|
return [start, end];
|
|
} catch (err) {
|
|
if (bench.opts.throws) {
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
|
|
const wrapWithInstrumentHooksAsync = async (fn, uri) => {
|
|
InstrumentHooks.startBenchmark();
|
|
const result = await fn();
|
|
InstrumentHooks.stopBenchmark();
|
|
InstrumentHooks.setExecutedBenchmark(process.pid, uri);
|
|
return result;
|
|
};
|
|
|
|
function printStatus(fn) {
|
|
const status = eval("%GetOptimizationStatus(fn)");
|
|
console.log(status.toString(2).padStart(12, "0"));
|
|
|
|
if (status & (1 << 0)) {
|
|
console.log("is function");
|
|
}
|
|
|
|
if (status & (1 << 1)) {
|
|
console.log("is never optimized");
|
|
}
|
|
|
|
if (status & (1 << 2)) {
|
|
console.log("is always optimized");
|
|
}
|
|
|
|
if (status & (1 << 3)) {
|
|
console.log("is maybe deoptimized");
|
|
}
|
|
|
|
if (status & (1 << 4)) {
|
|
console.log("is optimized");
|
|
}
|
|
|
|
if (status & (1 << 5)) {
|
|
console.log("is optimized by TurboFan");
|
|
}
|
|
|
|
if (status & (1 << 6)) {
|
|
console.log("is interpreted");
|
|
}
|
|
|
|
if (status & (1 << 7)) {
|
|
console.log("is marked for optimization");
|
|
}
|
|
|
|
if (status & (1 << 8)) {
|
|
console.log("is marked for concurrent optimization");
|
|
}
|
|
|
|
if (status & (1 << 9)) {
|
|
console.log("is optimizing concurrently");
|
|
}
|
|
|
|
if (status & (1 << 10)) {
|
|
console.log("is executing");
|
|
}
|
|
|
|
if (status & (1 << 11)) {
|
|
console.log("topmost frame is turbo fanned");
|
|
}
|
|
}
|
|
|
|
const runTaskAsync = async (task, uri) => {
|
|
const { fnOpts, fn } = task;
|
|
|
|
// Custom setup
|
|
await bench.opts.setup?.(task, "run");
|
|
|
|
await fnOpts?.beforeAll?.call(task, "run");
|
|
|
|
// Custom warmup
|
|
const samples = [];
|
|
while (samples.length < bench.opts.iterations - 1) {
|
|
samples.push(await iterationAsync(task));
|
|
}
|
|
|
|
const toRun = async () => {
|
|
await fnOpts?.beforeEach?.call(task, "run");
|
|
await fn();
|
|
await fnOpts?.afterEach?.call(task, "run");
|
|
};
|
|
|
|
await optimizeFunction(toRun);
|
|
|
|
console.log("optimize", printStatus(toRun));
|
|
|
|
await fnOpts?.beforeEach?.call(task, "run");
|
|
await mongoMeasurement.start(uri);
|
|
global.gc?.();
|
|
await wrapWithInstrumentHooksAsync(wrapFunctionWithFrame(fn, true), uri);
|
|
await mongoMeasurement.stop(uri);
|
|
await fnOpts?.afterEach?.call(task, "run");
|
|
console.log(`[Codspeed] ✔ Measured ${uri}`);
|
|
await fnOpts?.afterAll?.call(task, "run");
|
|
|
|
// Custom teardown
|
|
await bench.opts.teardown?.(task, "run");
|
|
|
|
logTaskCompletion(uri, taskCompletionMessage());
|
|
};
|
|
|
|
const iteration = (task) => {
|
|
try {
|
|
task.fnOpts.beforeEach?.call(task, "run");
|
|
const start = bench.opts.now();
|
|
task.fn();
|
|
const end = bench.opts.now() - start || 0;
|
|
task.fnOpts.afterEach?.call(this, "run");
|
|
return [start, end];
|
|
} catch (err) {
|
|
if (bench.opts.throws) {
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
|
|
const wrapWithInstrumentHooks = (fn, uri) => {
|
|
InstrumentHooks.startBenchmark();
|
|
const result = fn();
|
|
InstrumentHooks.stopBenchmark();
|
|
InstrumentHooks.setExecutedBenchmark(process.pid, uri);
|
|
return result;
|
|
};
|
|
|
|
const runTaskSync = (task, uri) => {
|
|
const { fnOpts, fn } = task;
|
|
|
|
// Custom setup
|
|
bench.opts.setup?.(task, "run");
|
|
|
|
fnOpts?.beforeAll?.call(task, "run");
|
|
|
|
// Custom warmup
|
|
const samples = [];
|
|
while (samples.length < bench.opts.iterations - 1) {
|
|
samples.push(iteration(task));
|
|
}
|
|
|
|
fnOpts?.beforeEach?.call(task, "run");
|
|
|
|
wrapWithInstrumentHooks(wrapFunctionWithFrame(fn, false), uri);
|
|
|
|
fnOpts?.afterEach?.call(task, "run");
|
|
console.log(`[Codspeed] ✔ Measured ${uri}`);
|
|
fnOpts?.afterAll?.call(task, "run");
|
|
|
|
// Custom teardown
|
|
bench.opts.teardown?.(task, "run");
|
|
|
|
logTaskCompletion(uri, taskCompletionMessage());
|
|
};
|
|
|
|
const finalizeAsyncRun = () => {
|
|
finalizeBenchRun();
|
|
};
|
|
const finalizeSyncRun = () => {
|
|
finalizeBenchRun();
|
|
};
|
|
|
|
bench.run = async () => {
|
|
setupBenchRun();
|
|
|
|
for (const task of bench.tasks) {
|
|
const uri = getTaskUri(task.bench, task.name, rootCallingFile);
|
|
await runTaskAsync(task, uri);
|
|
}
|
|
|
|
return finalizeAsyncRun();
|
|
};
|
|
|
|
bench.runSync = () => {
|
|
setupBenchRun();
|
|
|
|
for (const task of bench.tasks) {
|
|
const uri = getTaskUri(task.bench, task.name, rootCallingFile);
|
|
runTaskSync(task, uri);
|
|
}
|
|
|
|
return finalizeSyncRun();
|
|
};
|
|
} else if (codspeedRunnerMode === "walltime") {
|
|
// We don't need it
|
|
}
|
|
|
|
return bench;
|
|
};
|
|
|
|
const bench = await withCodSpeed(
|
|
new Bench({
|
|
now: hrtimeNow,
|
|
throws: true,
|
|
warmup: true,
|
|
warmupIterations: 2,
|
|
iterations: 8,
|
|
setup(task, mode) {
|
|
console.log(`Setup (${mode} mode): ${task.name}`);
|
|
},
|
|
teardown(task, mode) {
|
|
console.log(`Teardown (${mode} mode): ${task.name}`);
|
|
}
|
|
})
|
|
);
|
|
|
|
async function registerSuite(bench, test, baselines) {
|
|
const testDirectory = path.join(casesPath, test);
|
|
const optionsPath = path.resolve(testDirectory, "options.mjs");
|
|
|
|
let options = {};
|
|
|
|
try {
|
|
options = await import(`${pathToFileURL(optionsPath)}`);
|
|
} catch (_err) {
|
|
// Ignore
|
|
}
|
|
|
|
if (typeof options.setup !== "undefined") {
|
|
await options.setup();
|
|
}
|
|
|
|
if (test.includes("-unit")) {
|
|
const fullBenchName = `unit benchmark "${test}"`;
|
|
|
|
console.log(`Register: ${fullBenchName}`);
|
|
|
|
const benchmarkPath = path.resolve(testDirectory, "index.bench.mjs");
|
|
const registerBenchmarks = await import(`${pathToFileURL(benchmarkPath)}`);
|
|
|
|
registerBenchmarks.default(bench);
|
|
|
|
return;
|
|
}
|
|
|
|
const realConfig = (
|
|
await import(
|
|
`${pathToFileURL(path.join(testDirectory, "webpack.config.js"))}`
|
|
)
|
|
).default;
|
|
|
|
await Promise.all(
|
|
baselines.map(async (baseline) => {
|
|
const webpack = await baseline.webpack();
|
|
|
|
await Promise.all(
|
|
scenarios.map(async (scenario) => {
|
|
const config = buildConfiguration(
|
|
test,
|
|
baseline,
|
|
realConfig,
|
|
scenario,
|
|
testDirectory
|
|
);
|
|
|
|
const stringifiedScenario = JSON.stringify(scenario);
|
|
const benchName = `benchmark "${test}", scenario '${stringifiedScenario}'${LAST_COMMIT ? "" : ` ${baseline.name} (${baseline.rev})`}`;
|
|
const fullBenchName = `benchmark "${test}", scenario '${stringifiedScenario}' ${baseline.name} ${baseline.rev ? `(${baseline.rev})` : ""}`;
|
|
|
|
console.log(`Register: ${fullBenchName}`);
|
|
|
|
if (scenario.watch) {
|
|
const entry = path.resolve(config.entry);
|
|
const originalEntryContent = await fs.readFile(entry, "utf8");
|
|
|
|
let watching;
|
|
let next;
|
|
|
|
const watchCallback = (err, stats) => {
|
|
if (next) {
|
|
next(err, stats);
|
|
}
|
|
};
|
|
|
|
bench.add(
|
|
benchName,
|
|
async () => {
|
|
console.time(`Time: ${benchName}`);
|
|
|
|
let resolve;
|
|
let reject;
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
|
|
next = (err, stats) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
if (stats.hasWarnings() || stats.hasErrors()) {
|
|
reject(new Error(stats.toString()));
|
|
return;
|
|
}
|
|
|
|
// Construct and print stats to be more accurate with real life projects
|
|
stats.toString();
|
|
resolve();
|
|
console.timeEnd(`Time: ${benchName}`);
|
|
};
|
|
|
|
await new Promise((resolve, reject) => {
|
|
writeFile(
|
|
entry,
|
|
`${originalEntryContent};console.log('watch test')`,
|
|
(err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
}
|
|
);
|
|
});
|
|
|
|
await promise;
|
|
},
|
|
{
|
|
async beforeAll() {
|
|
this.collectBy = `${test}, scenario '${stringifiedScenario}'`;
|
|
|
|
let resolve;
|
|
let reject;
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
|
|
next = (err, stats) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
if (stats.hasWarnings() || stats.hasErrors()) {
|
|
reject(new Error(stats.toString()));
|
|
return;
|
|
}
|
|
|
|
// Construct and print stats to be more accurate with real life projects
|
|
stats.toString();
|
|
resolve();
|
|
};
|
|
|
|
if (GENERATE_PROFILE) {
|
|
await withProfiling(
|
|
benchName,
|
|
async () =>
|
|
(watching = await runWatch(
|
|
webpack,
|
|
config,
|
|
watchCallback
|
|
))
|
|
);
|
|
} else {
|
|
watching = await runWatch(webpack, config, watchCallback);
|
|
}
|
|
|
|
// Make an extra fs call to warn up filesystem caches
|
|
// Also wait a first run callback
|
|
await new Promise((resolve, reject) => {
|
|
writeFile(
|
|
entry,
|
|
`${originalEntryContent};console.log('watch test')`,
|
|
(err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
}
|
|
);
|
|
});
|
|
|
|
await promise;
|
|
},
|
|
async afterAll() {
|
|
// Close watching
|
|
await new Promise((resolve, reject) => {
|
|
if (watching) {
|
|
watching.close((closeErr) => {
|
|
if (closeErr) {
|
|
reject(closeErr);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Write original content
|
|
await new Promise((resolve, reject) => {
|
|
writeFile(entry, originalEntryContent, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
bench.add(
|
|
benchName,
|
|
async () => {
|
|
if (GENERATE_PROFILE) {
|
|
await withProfiling(benchName, () =>
|
|
runWebpack(webpack, config)
|
|
);
|
|
} else {
|
|
console.time(`Time: ${benchName}`);
|
|
await runWebpack(webpack, config);
|
|
console.timeEnd(`Time: ${benchName}`);
|
|
}
|
|
},
|
|
{
|
|
beforeAll() {
|
|
this.collectBy = `${test}, scenario '${stringifiedScenario}'`;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
})
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
await fs.rm(baseOutputPath, { recursive: true, force: true });
|
|
|
|
const FILTER =
|
|
typeof process.env.FILTER !== "undefined"
|
|
? new RegExp(process.env.FILTER)
|
|
: undefined;
|
|
|
|
const NEGATIVE_FILTER =
|
|
typeof process.env.NEGATIVE_FILTER !== "undefined"
|
|
? new RegExp(process.env.NEGATIVE_FILTER)
|
|
: undefined;
|
|
|
|
const casesPath = path.join(__dirname, "benchmarkCases");
|
|
const allBenchmarks = (await fs.readdir(casesPath))
|
|
.filter(
|
|
(item) =>
|
|
!item.includes("_") &&
|
|
(FILTER ? FILTER.test(item) : true) &&
|
|
(NEGATIVE_FILTER ? !NEGATIVE_FILTER.test(item) : true)
|
|
)
|
|
.sort((a, b) => a.localeCompare(b));
|
|
|
|
const benchmarks = allBenchmarks.filter((item) => !item.includes("-long"));
|
|
const longBenchmarks = allBenchmarks.filter((item) => item.includes("-long"));
|
|
const i = Math.floor(benchmarks.length / longBenchmarks.length);
|
|
|
|
for (const [index, value] of longBenchmarks.entries()) {
|
|
benchmarks.splice(index * i, 0, value);
|
|
}
|
|
|
|
const shard =
|
|
typeof process.env.SHARD !== "undefined"
|
|
? process.env.SHARD.split("/").map((item) => Number.parseInt(item, 10))
|
|
: [1, 1];
|
|
|
|
if (
|
|
typeof shard[0] === "undefined" ||
|
|
typeof shard[1] === "undefined" ||
|
|
shard[0] > shard[1] ||
|
|
shard[0] <= 0 ||
|
|
shard[1] <= 0
|
|
) {
|
|
throw new Error(
|
|
`Invalid \`SHARD\` value - it should be less then a part and more than zero, shard part is ${shard[0]}, count of shards is ${shard[1]}`
|
|
);
|
|
}
|
|
|
|
function splitToNChunks(array, n) {
|
|
const result = [];
|
|
|
|
for (let i = n; i > 0; i--) {
|
|
result.push(array.splice(0, Math.ceil(array.length / i)));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
const countOfBenchmarks = benchmarks.length;
|
|
|
|
if (countOfBenchmarks < shard[1]) {
|
|
throw new Error(
|
|
`Shard upper limit is more than count of benchmarks, count of benchmarks is ${countOfBenchmarks}, shard is ${shard[1]}`
|
|
);
|
|
}
|
|
|
|
await Promise.all(
|
|
splitToNChunks(benchmarks, shard[1])[shard[0] - 1].map((benchmark) =>
|
|
registerSuite(bench, benchmark, baselines)
|
|
)
|
|
);
|
|
|
|
function formatNumber(value, precision, fractionDigits) {
|
|
return Math.abs(value) >= 10 ** precision
|
|
? value.toFixed()
|
|
: Math.abs(value) < 10 ** (precision - fractionDigits)
|
|
? value.toFixed(fractionDigits)
|
|
: value.toPrecision(precision);
|
|
}
|
|
|
|
const US_PER_MS = 10 ** 3;
|
|
const NS_PER_MS = 10 ** 6;
|
|
|
|
function formatTime(value) {
|
|
const toType =
|
|
Math.round(value) > 0
|
|
? "ms"
|
|
: Math.round(value * US_PER_MS) / US_PER_MS > 0
|
|
? "µs"
|
|
: "ns";
|
|
|
|
switch (toType) {
|
|
case "ms": {
|
|
return `${formatNumber(value, 5, 2)} ms`;
|
|
}
|
|
case "µs": {
|
|
return `${formatNumber(value * US_PER_MS, 5, 2)} µs`;
|
|
}
|
|
case "ns": {
|
|
return `${formatNumber(value * NS_PER_MS, 5, 2)} ns`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const statsByTests = new Map();
|
|
|
|
bench.addEventListener("cycle", (event) => {
|
|
const task = event.task;
|
|
const runs = task.runs;
|
|
const nSqrt = Math.sqrt(runs);
|
|
const z = tDistribution(runs - 1);
|
|
const { latency } = task.result;
|
|
const minConfidence = latency.mean - (z * latency.sd) / nSqrt;
|
|
const maxConfidence = latency.mean + (z * latency.sd) / nSqrt;
|
|
const mean = formatTime(latency.mean);
|
|
const deviation = formatTime(latency.sd);
|
|
const minConfidenceFormatted = formatTime(minConfidence);
|
|
const maxConfidenceFormatted = formatTime(maxConfidence);
|
|
const confidence = `${mean} ± ${deviation} [${minConfidenceFormatted}; ${maxConfidenceFormatted}]`;
|
|
const text = `${task.name} ${confidence}`;
|
|
|
|
const collectBy = task.collectBy;
|
|
const allStats = statsByTests.get(collectBy);
|
|
|
|
console.log(`Cycle: ${task.name} ${confidence} (${runs} runs sampled)`);
|
|
|
|
const info = { ...latency, text, minConfidence, maxConfidence };
|
|
|
|
if (!allStats) {
|
|
statsByTests.set(collectBy, [info]);
|
|
return;
|
|
}
|
|
|
|
allStats.push(info);
|
|
|
|
const firstStats = allStats[0];
|
|
const secondStats = allStats[1];
|
|
|
|
console.log(
|
|
`Result: ${firstStats.text} is ${Math.round(
|
|
(secondStats.mean / firstStats.mean) * 100 - 100
|
|
)}% ${secondStats.maxConfidence < firstStats.minConfidence ? "slower than" : secondStats.minConfidence > firstStats.maxConfidence ? "faster than" : "the same as"} ${secondStats.text}`
|
|
);
|
|
});
|
|
|
|
bench.run();
|