2025-05-14 19:46:56 +08:00
import { constants , writeFile } from "fs" ;
2025-07-03 17:06:45 +08:00
import fs from "fs/promises" ;
2025-08-12 07:41:06 +08:00
import { Session } from "inspector" ;
2025-07-03 17:06:45 +08:00
import path from "path" ;
2025-05-01 04:52:14 +08:00
import { fileURLToPath , pathToFileURL } from "url" ;
2025-09-30 03:54:19 +08:00
import {
InstrumentHooks ,
getCodspeedRunnerMode ,
getGitDir ,
getV8Flags ,
mongoMeasurement ,
setupCore ,
teardownCore
} from "@codspeed/core" ;
2025-07-03 17:06:45 +08:00
import { simpleGit } from "simple-git" ;
import { Bench , hrtimeNow } from "tinybench" ;
2025-04-22 02:40:24 +08:00
2025-05-28 22:40:54 +08:00
const _ _dirname = path . dirname ( fileURLToPath ( import . meta . url ) ) ;
2025-04-22 02:40:24 +08:00
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*$/ ;
2025-04-29 23:45:18 +08:00
const checkV8Flags = ( ) => {
const requiredFlags = getV8Flags ( ) ;
const actualFlags = process . execArgv ;
const missingFlags = requiredFlags . filter (
2025-07-17 00:13:14 +08:00
( flag ) => ! actualFlags . includes ( flag )
2025-04-29 23:45:18 +08:00
) ;
if ( missingFlags . length > 0 ) {
console . warn ( ` Missing required flags: ${ missingFlags . join ( ", " ) } ` ) ;
}
} ;
checkV8Flags ( ) ;
2025-05-14 19:46:56 +08:00
const LAST _COMMIT = typeof process . env . LAST _COMMIT !== "undefined" ;
2025-08-12 07:41:06 +08:00
const GENERATE _PROFILE = typeof process . env . PROFILE !== "undefined" ;
2025-04-22 05:17:46 +08:00
2025-04-22 20:42:33 +08:00
/ * *
* @ param { ( string | undefined ) [ ] } revList rev list
* @ returns { Promise < string > } head
* /
2025-04-22 02:40:24 +08:00
async function getHead ( revList ) {
if ( typeof process . env . HEAD !== "undefined" ) {
return process . env . HEAD ;
}
2025-05-01 04:52:14 +08:00
// On CI we take the latest commit `merge commit` as a head
2025-04-22 02:40:24 +08:00
if ( revList [ 3 ] ) {
return revList [ 3 ] ;
}
2025-05-01 04:52:14 +08:00
// Otherwise we take the latest commit
2025-04-22 02:40:24 +08:00
return revList [ 1 ] ;
}
2025-04-22 20:42:33 +08:00
/ * *
2025-05-01 04:52:14 +08:00
* @ param { string } head head
2025-04-22 20:42:33 +08:00
* @ param { ( string | undefined ) [ ] } revList rev list
* @ returns { Promise < string > } base
* /
2025-05-01 04:52:14 +08:00
async function getBase ( head , revList ) {
2025-04-22 02:40:24 +08:00
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" ] ) ;
2025-04-22 05:17:46 +08:00
if ( branchName . trim ( ) !== "main" ) {
2025-04-22 02:40:24 +08:00
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" ) ;
}
2025-05-01 04:52:14 +08:00
if ( head === revList [ 1 ] ) {
return revList [ 2 ] ;
}
2025-04-22 02:40:24 +08:00
return revList [ 1 ] ;
}
return revList [ 2 ] ;
}
2025-04-22 20:42:33 +08:00
/ * *
* @ returns { Promise < { name : string , rev : string } [ ] > } baseline revs
* /
2025-04-22 02:40:24 +08:00
async function getBaselineRevs ( ) {
2025-05-14 19:46:56 +08:00
if ( LAST _COMMIT ) {
return [
{
name : "HEAD"
}
] ;
}
2025-04-22 02:40:24 +08:00
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 ) ;
2025-05-01 04:52:14 +08:00
const base = await getBase ( head , revList ) ;
2025-04-22 02:40:24 +08:00
if ( ! head || ! base ) {
throw new Error ( "No baseline found" ) ;
}
return [
{
name : "HEAD" ,
rev : head
} ,
{
name : "BASE" ,
rev : base
}
] ;
}
2025-04-22 20:42:33 +08:00
/ * *
* @ param { number } n number of runs
* @ returns { number } distribution
* /
2025-04-22 02:40:24 +08:00
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" ) ;
2025-05-14 19:46:56 +08:00
const baselineRevisions = await getBaselineRevs ( ) ;
2025-04-22 02:40:24 +08:00
try {
await fs . mkdir ( baselinesPath , { recursive : true } ) ;
} catch ( _err ) { } // eslint-disable-line no-empty
2025-05-14 19:46:56 +08:00
const baselines = [ ] ;
2025-04-22 02:40:24 +08:00
for ( const baselineInfo of baselineRevisions ) {
2025-04-22 20:42:33 +08:00
/ * *
* @ returns { void }
* /
2025-04-29 23:45:18 +08:00
function addBaseline ( ) {
2025-04-22 02:40:24 +08:00
baselines . push ( {
name : baselineInfo . name ,
rev : baselineRevision ,
2025-05-01 04:52:14 +08:00
webpack : async ( ) => {
2025-04-29 23:45:18 +08:00
const webpack = (
await import (
2025-07-02 20:10:54 +08:00
pathToFileURL ( path . resolve ( baselinePath , "./lib/index.js" ) )
2025-04-29 23:45:18 +08:00
)
) . default ;
return webpack ;
}
2025-04-22 02:40:24 +08:00
} ) ;
}
const baselineRevision = baselineInfo . rev ;
2025-05-14 19:46:56 +08:00
const baselinePath =
baselineRevision === undefined
? path . resolve ( _ _dirname , "../" )
: path . resolve ( baselinesPath , baselineRevision ) ;
2025-04-22 02:40:24 +08:00
try {
2025-04-22 05:17:46 +08:00
await fs . access ( path . resolve ( baselinePath , ".git" ) , constants . R _OK ) ;
2025-04-22 02:40:24 +08:00
} 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 {
2025-04-29 23:45:18 +08:00
addBaseline ( ) ;
2025-04-22 02:40:24 +08:00
}
}
2025-09-30 03:54:19 +08:00
const baseOutputPath = path . join ( _ _dirname , "js" , "benchmark" ) ;
2025-05-01 04:52:14 +08:00
function buildConfiguration (
test ,
baseline ,
realConfig ,
scenario ,
testDirectory
) {
const { watch , ... rest } = scenario ;
const config = structuredClone ( { ... realConfig , ... rest } ) ;
2025-05-14 19:46:56 +08:00
config . entry = path . resolve (
testDirectory ,
config . entry
? /\.(c|m)?js$/ . test ( config . entry )
? config . entry
: ` ${ config . entry } .js `
: "./index.js"
) ;
2025-05-01 04:52:14 +08:00
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 } `
) ;
2025-05-14 19:46:56 +08:00
config . plugins = config . plugins || [ ] ;
2025-05-01 04:52:14 +08:00
if ( config . cache ) {
config . cache . cacheDirectory = path . resolve ( config . output . path , ".cache" ) ;
}
2025-07-24 04:48:44 +08:00
if ( watch ) {
config . cache = {
type : "memory" ,
maxGenerations : 1
} ;
}
2025-05-01 04:52:14 +08:00
return config ;
}
2025-08-12 07:41:06 +08:00
// 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 ( ) ;
} ) ;
} ) ;
} ) ;
}
2025-08-28 18:49:41 +08:00
async function runWatch ( webpack , config , callback ) {
2025-08-12 07:41:06 +08:00
const compiler = webpack ( config ) ;
2025-08-28 18:49:41 +08:00
return compiler . watch ( { } , callback ) ;
2025-05-01 04:52:14 +08:00
}
2025-05-15 23:43:29 +08:00
const scenarios = [
{
name : "mode-development" ,
mode : "development"
} ,
{
name : "mode-development-rebuild" ,
mode : "development" ,
watch : true
} ,
{
name : "mode-production" ,
mode : "production"
}
] ;
2025-09-30 03:54:19 +08:00
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 ;
}
2025-05-01 04:52:14 +08:00
2025-09-30 03:54:19 +08:00
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 ) ;
}
2025-07-24 04:48:44 +08:00
2025-09-30 03:54:19 +08:00
const taskUriMap = new WeakMap ( ) ;
function getOrCreateUriMap ( bench ) {
let uriMap = taskUriMap . get ( bench ) ;
if ( ! uriMap ) {
uriMap = new Map ( ) ;
taskUriMap . set ( bench , uriMap ) ;
2025-07-24 04:48:44 +08:00
}
2025-09-30 03:54:19 +08:00
return uriMap ;
}
2025-07-24 04:48:44 +08:00
2025-09-30 03:54:19 +08:00
function getTaskUri ( bench , taskName , rootCallingFile ) {
const uriMap = taskUriMap . get ( bench ) ;
return uriMap ? . get ( taskName ) || ` ${ rootCallingFile } :: ${ taskName } ` ;
}
2025-07-24 04:48:44 +08:00
2025-09-30 03:54:19 +08:00
const withCodSpeed = async ( /** @type {import("tinybench").Bench} */ bench ) => {
const codspeedRunnerMode = getCodspeedRunnerMode ( ) ;
if ( codspeedRunnerMode === "disabled" ) {
return bench ;
}
2025-07-24 04:48:44 +08:00
const rawAdd = bench . add ;
2025-09-30 03:54:19 +08:00
const uriMap = getOrCreateUriMap ( bench ) ;
2025-07-24 04:48:44 +08:00
bench . add = ( name , fn , opts ) => {
const callingFile = getCallingFile ( ) ;
2025-09-30 03:54:19 +08:00
let uri = callingFile ;
if ( bench . name !== undefined ) {
uri += ` :: ${ bench . name } ` ;
}
uri += ` :: ${ name } ` ;
uriMap . set ( name , uri ) ;
return rawAdd . bind ( bench ) ( name , fn , opts ) ;
2025-07-24 04:48:44 +08:00
} ;
const rootCallingFile = getCallingFile ( ) ;
2025-09-30 03:54:19 +08:00
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 ;
2025-07-24 04:48:44 +08:00
}
}
2025-09-30 03:54:19 +08:00
} ;
const wrapWithInstrumentHooksAsync = async ( fn , uri ) => {
InstrumentHooks . startBenchmark ( ) ;
const result = await fn ( ) ;
InstrumentHooks . stopBenchmark ( ) ;
InstrumentHooks . setExecutedBenchmark ( process . pid , uri ) ;
return result ;
} ;
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
2025-10-03 02:17:30 +08:00
// We don't run `optimizeFunction` because our function is never optimized, instead we just warmup webpack
2025-09-30 03:54:19 +08:00
const samples = [ ] ;
2025-10-03 02:17:30 +08:00
2025-09-30 03:54:19 +08:00
while ( samples . length < bench . opts . iterations - 1 ) {
2025-09-30 03:58:58 +08:00
samples . push ( await iterationAsync ( task ) ) ;
2025-07-24 04:48:44 +08:00
}
2025-09-30 03:54:19 +08:00
await fnOpts ? . beforeEach ? . call ( task , "run" ) ;
2025-07-24 04:48:44 +08:00
await mongoMeasurement . start ( uri ) ;
2025-09-30 03:54:19 +08:00
global . gc ? . ( ) ;
await wrapWithInstrumentHooksAsync ( wrapFunctionWithFrame ( fn , true ) , uri ) ;
2025-07-24 04:48:44 +08:00
await mongoMeasurement . stop ( uri ) ;
2025-09-30 03:54:19 +08:00
await fnOpts ? . afterEach ? . call ( task , "run" ) ;
2025-07-24 04:48:44 +08:00
console . log ( ` [Codspeed] ✔ Measured ${ uri } ` ) ;
2025-09-30 03:54:19 +08:00
await fnOpts ? . afterAll ? . call ( task , "run" ) ;
2025-07-24 04:48:44 +08:00
2025-09-30 03:54:19 +08:00
// Custom teardown
2025-07-24 04:48:44 +08:00
await bench . opts . teardown ? . ( task , "run" ) ;
2025-09-30 03:54:19 +08:00
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 = [ ] ;
2025-10-03 02:17:30 +08:00
2025-09-30 03:54:19 +08:00
while ( samples . length < bench . opts . iterations - 1 ) {
2025-09-30 03:58:58 +08:00
samples . push ( iteration ( task ) ) ;
2025-09-30 03:54:19 +08:00
}
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" ) {
2025-10-02 00:10:11 +08:00
// We don't need it
2025-09-30 03:54:19 +08:00
}
2025-07-24 04:48:44 +08:00
return bench ;
} ;
const bench = await withCodSpeed (
2025-05-15 23:43:29 +08:00
new Bench ( {
now : hrtimeNow ,
2025-05-20 19:41:46 +08:00
throws : true ,
warmup : true ,
2025-07-24 04:48:44 +08:00
warmupIterations : 2 ,
iterations : 8 ,
setup ( task , mode ) {
console . log ( ` Setup ( ${ mode } mode): ${ task . name } ` ) ;
} ,
teardown ( task , mode ) {
console . log ( ` Teardown ( ${ mode } mode): ${ task . name } ` ) ;
}
2025-05-15 23:43:29 +08:00
} )
) ;
async function registerSuite ( bench , test , baselines ) {
2025-05-01 04:52:14 +08:00
const testDirectory = path . join ( casesPath , test ) ;
2025-05-14 19:46:56 +08:00
const optionsPath = path . resolve ( testDirectory , "options.mjs" ) ;
2025-05-01 04:52:14 +08:00
2025-05-14 19:46:56 +08:00
let options = { } ;
2025-05-01 04:52:14 +08:00
try {
2025-05-14 19:46:56 +08:00
options = await import ( ` ${ pathToFileURL ( optionsPath ) } ` ) ;
2025-05-01 04:52:14 +08:00
} catch ( _err ) {
2025-05-14 19:46:56 +08:00
// Ignore
2025-05-01 04:52:14 +08:00
}
2025-05-14 19:46:56 +08:00
if ( typeof options . setup !== "undefined" ) {
await options . setup ( ) ;
}
if ( test . includes ( "-unit" ) ) {
2025-05-15 23:43:29 +08:00
const fullBenchName = ` unit benchmark " ${ test } " ` ;
2025-05-14 19:46:56 +08:00
2025-05-15 23:43:29 +08:00
console . log ( ` Register: ${ fullBenchName } ` ) ;
2025-05-14 19:46:56 +08:00
const benchmarkPath = path . resolve ( testDirectory , "index.bench.mjs" ) ;
const registerBenchmarks = await import ( ` ${ pathToFileURL ( benchmarkPath ) } ` ) ;
2025-05-15 23:43:29 +08:00
registerBenchmarks . default ( bench ) ;
2025-05-14 19:46:56 +08:00
return ;
2025-04-22 02:40:24 +08:00
}
2025-05-01 04:52:14 +08:00
const realConfig = (
await import (
2025-07-02 20:10:54 +08:00
` ${ pathToFileURL ( path . join ( testDirectory , "webpack.config.js" ) ) } `
2025-05-01 04:52:14 +08:00
)
) . default ;
await Promise . all (
2025-07-17 00:13:14 +08:00
baselines . map ( async ( baseline ) => {
2025-05-01 04:52:14 +08:00
const webpack = await baseline . webpack ( ) ;
await Promise . all (
2025-07-17 00:13:14 +08:00
scenarios . map ( async ( scenario ) => {
2025-05-01 04:52:14 +08:00
const config = buildConfiguration (
test ,
baseline ,
realConfig ,
scenario ,
testDirectory
) ;
2025-05-15 23:43:29 +08:00
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 } ) ` : "" } ` ;
2025-05-01 04:52:14 +08:00
2025-05-15 23:43:29 +08:00
console . log ( ` Register: ${ fullBenchName } ` ) ;
2025-05-01 04:52:14 +08:00
if ( scenario . watch ) {
2025-05-15 23:43:29 +08:00
const entry = path . resolve ( config . entry ) ;
2025-07-02 20:10:54 +08:00
const originalEntryContent = await fs . readFile ( entry , "utf8" ) ;
2025-05-01 04:52:14 +08:00
2025-05-15 23:43:29 +08:00
let watching ;
2025-08-28 18:49:41 +08:00
let next ;
const watchCallback = ( err , stats ) => {
if ( next ) {
next ( err , stats ) ;
}
} ;
2025-05-01 04:52:14 +08:00
2025-05-15 23:43:29 +08:00
bench . add (
benchName ,
async ( ) => {
2025-07-24 04:48:44 +08:00
console . time ( ` Time: ${ benchName } ` ) ;
2025-08-28 18:49:41 +08:00
let resolve ;
let reject ;
const promise = new Promise ( ( res , rej ) => {
resolve = res ;
reject = rej ;
2025-05-14 19:46:56 +08:00
} ) ;
2025-08-28 18:49:41 +08:00
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 } ` ) ;
} ;
2025-05-15 23:43:29 +08:00
await new Promise ( ( resolve , reject ) => {
writeFile (
entry ,
` ${ originalEntryContent } ;console.log('watch test') ` ,
2025-07-17 00:13:14 +08:00
( err ) => {
2025-05-15 23:43:29 +08:00
if ( err ) {
reject ( err ) ;
2025-08-28 18:49:41 +08:00
return ;
2025-05-15 23:43:29 +08:00
}
2025-08-28 18:49:41 +08:00
resolve ( ) ;
2025-05-01 04:52:14 +08:00
}
2025-05-15 23:43:29 +08:00
) ;
2025-05-01 04:52:14 +08:00
} ) ;
2025-08-28 18:49:41 +08:00
await promise ;
2025-05-15 23:43:29 +08:00
} ,
{
async beforeAll ( ) {
this . collectBy = ` ${ test } , scenario ' ${ stringifiedScenario } ' ` ;
2025-08-28 18:49:41 +08:00
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 ( ) ;
} ;
2025-08-12 07:41:06 +08:00
if ( GENERATE _PROFILE ) {
await withProfiling (
benchName ,
2025-08-28 18:49:41 +08:00
async ( ) =>
( watching = await runWatch (
webpack ,
config ,
watchCallback
) )
2025-08-12 07:41:06 +08:00
) ;
} else {
2025-08-28 18:49:41 +08:00
watching = await runWatch ( webpack , config , watchCallback ) ;
2025-08-12 07:41:06 +08:00
}
2025-08-28 18:49:41 +08:00
// Make an extra fs call to warn up filesystem caches
// Also wait a first run callback
2025-05-15 23:43:29 +08:00
await new Promise ( ( resolve , reject ) => {
2025-08-28 18:49:41 +08:00
writeFile (
entry ,
` ${ originalEntryContent } ;console.log('watch test') ` ,
( err ) => {
if ( err ) {
reject ( err ) ;
return ;
}
2025-05-15 23:43:29 +08:00
2025-08-28 18:49:41 +08:00
resolve ( ) ;
}
) ;
2025-05-15 23:43:29 +08:00
} ) ;
2025-08-28 18:49:41 +08:00
await promise ;
2025-05-15 23:43:29 +08:00
} ,
async afterAll ( ) {
2025-08-28 18:49:41 +08:00
// Close watching
2025-05-15 23:43:29 +08:00
await new Promise ( ( resolve , reject ) => {
if ( watching ) {
2025-07-17 00:13:14 +08:00
watching . close ( ( closeErr ) => {
2025-05-15 23:43:29 +08:00
if ( closeErr ) {
reject ( closeErr ) ;
return ;
}
resolve ( ) ;
} ) ;
}
} ) ;
2025-08-28 18:49:41 +08:00
// Write original content
await new Promise ( ( resolve , reject ) => {
writeFile ( entry , originalEntryContent , ( err ) => {
if ( err ) {
reject ( err ) ;
return ;
}
resolve ( ) ;
} ) ;
} ) ;
2025-05-15 23:43:29 +08:00
}
2025-05-14 19:46:56 +08:00
}
2025-05-15 23:43:29 +08:00
) ;
2025-05-14 19:46:56 +08:00
} else {
2025-05-15 23:43:29 +08:00
bench . add (
benchName ,
async ( ) => {
2025-08-12 07:41:06 +08:00
if ( GENERATE _PROFILE ) {
await withProfiling ( benchName , ( ) =>
runWebpack ( webpack , config )
) ;
} else {
2025-07-24 04:48:44 +08:00
console . time ( ` Time: ${ benchName } ` ) ;
2025-08-12 07:41:06 +08:00
await runWebpack ( webpack , config ) ;
console . timeEnd ( ` Time: ${ benchName } ` ) ;
}
2025-05-15 23:43:29 +08:00
} ,
{
beforeAll ( ) {
this . collectBy = ` ${ test } , scenario ' ${ stringifiedScenario } ' ` ;
}
2025-05-01 04:52:14 +08:00
}
2025-05-15 23:43:29 +08:00
) ;
2025-05-14 19:46:56 +08:00
}
2025-05-01 04:52:14 +08:00
} )
) ;
} )
) ;
2025-04-29 23:45:18 +08:00
}
2025-05-01 04:52:14 +08:00
await fs . rm ( baseOutputPath , { recursive : true , force : true } ) ;
2025-05-14 19:46:56 +08:00
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 ;
2025-04-29 23:45:18 +08:00
const casesPath = path . join ( _ _dirname , "benchmarkCases" ) ;
2025-05-01 04:52:14 +08:00
const allBenchmarks = ( await fs . readdir ( casesPath ) )
2025-05-14 19:46:56 +08:00
. filter (
2025-07-17 00:13:14 +08:00
( item ) =>
2025-05-14 19:46:56 +08:00
! item . includes ( "_" ) &&
( FILTER ? FILTER . test ( item ) : true ) &&
( NEGATIVE _FILTER ? ! NEGATIVE _FILTER . test ( item ) : true )
)
2025-05-01 04:52:14 +08:00
. sort ( ( a , b ) => a . localeCompare ( b ) ) ;
2025-04-29 23:45:18 +08:00
2025-07-17 00:13:14 +08:00
const benchmarks = allBenchmarks . filter ( ( item ) => ! item . includes ( "-long" ) ) ;
const longBenchmarks = allBenchmarks . filter ( ( item ) => item . includes ( "-long" ) ) ;
2025-05-01 04:52:14 +08:00
const i = Math . floor ( benchmarks . length / longBenchmarks . length ) ;
2025-04-29 23:45:18 +08:00
2025-05-01 04:52:14 +08:00
for ( const [ index , value ] of longBenchmarks . entries ( ) ) {
benchmarks . splice ( index * i , 0 , value ) ;
}
2025-04-29 23:45:18 +08:00
2025-05-01 04:52:14 +08:00
const shard =
typeof process . env . SHARD !== "undefined"
2025-07-17 00:13:14 +08:00
? process . env . SHARD . split ( "/" ) . map ( ( item ) => Number . parseInt ( item , 10 ) )
2025-05-01 04:52:14 +08:00
: [ 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 ) ) ) ;
2025-04-29 23:45:18 +08:00
}
2025-05-01 04:52:14 +08:00
return result ;
2025-04-29 23:45:18 +08:00
}
2025-05-01 04:52:14 +08:00
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 ] } `
) ;
2025-04-29 23:45:18 +08:00
}
2025-05-01 04:52:14 +08:00
await Promise . all (
2025-07-17 00:13:14 +08:00
splitToNChunks ( benchmarks , shard [ 1 ] ) [ shard [ 0 ] - 1 ] . map ( ( benchmark ) =>
2025-05-15 23:43:29 +08:00
registerSuite ( bench , benchmark , baselines )
2025-05-01 04:52:14 +08:00
)
) ;
2025-05-15 23:43:29 +08:00
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" ;
2025-05-14 19:46:56 +08:00
switch ( toType ) {
case "ms" : {
2025-05-15 23:43:29 +08:00
return ` ${ formatNumber ( value , 5 , 2 ) } ms ` ;
2025-05-14 19:46:56 +08:00
}
case "µs" : {
2025-05-15 23:43:29 +08:00
return ` ${ formatNumber ( value * US _PER _MS , 5 , 2 ) } µs ` ;
2025-05-14 19:46:56 +08:00
}
case "ns" : {
2025-05-15 23:43:29 +08:00
return ` ${ formatNumber ( value * NS _PER _MS , 5 , 2 ) } ns ` ;
2025-05-14 19:46:56 +08:00
}
}
}
2025-04-29 23:45:18 +08:00
const statsByTests = new Map ( ) ;
2025-07-17 00:13:14 +08:00
bench . addEventListener ( "cycle" , ( event ) => {
2025-05-15 23:43:29 +08:00
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 ;
2025-05-01 04:52:14 +08:00
const allStats = statsByTests . get ( collectBy ) ;
2025-07-24 04:48:44 +08:00
console . log ( ` Cycle: ${ task . name } ${ confidence } ( ${ runs } runs sampled) ` ) ;
2025-05-15 23:43:29 +08:00
const info = { ... latency , text , minConfidence , maxConfidence } ;
2025-04-29 23:45:18 +08:00
if ( ! allStats ) {
2025-05-15 23:43:29 +08:00
statsByTests . set ( collectBy , [ info ] ) ;
2025-04-29 23:45:18 +08:00
return ;
}
2025-05-15 23:43:29 +08:00
allStats . push ( info ) ;
2025-04-29 23:45:18 +08:00
2025-05-14 19:46:56 +08:00
const firstStats = allStats [ 0 ] ;
const secondStats = allStats [ 1 ] ;
2025-04-29 23:45:18 +08:00
console . log (
2025-05-14 19:46:56 +08:00
` 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 } `
2025-04-29 23:45:18 +08:00
) ;
} ) ;
2025-07-24 04:48:44 +08:00
bench . run ( ) ;