2025-04-22 02:40:24 +08:00
"use strict" ;
import path from "path" ;
import fs from "fs/promises" ;
2025-04-29 23:45:18 +08:00
import { constants } from "fs" ;
2025-04-22 02:40:24 +08:00
import Benchmark from "benchmark" ;
2025-04-29 23:45:18 +08:00
import { remove } from "./helpers/remove.js" ;
2025-04-22 05:17:46 +08:00
import { dirname } from "path" ;
import { fileURLToPath } from "url" ;
2025-04-22 02:40:24 +08:00
import simpleGit from "simple-git" ;
2025-04-29 23:45:18 +08:00
import { withCodSpeed } from "@codspeed/benchmark.js-plugin" ;
2025-04-22 02:40:24 +08:00
const _ _dirname = 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*$/ ;
2025-04-29 23:45:18 +08:00
const getV8Flags = ( ) => {
const nodeVersionMajor = Number . parseInt (
process . version . slice ( 1 ) . split ( "." ) [ 0 ]
) ;
const flags = [
"--hash-seed=1" ,
"--random-seed=1" ,
"--no-opt" ,
"--predictable" ,
"--predictable-gc-schedule" ,
"--interpreted-frames-native-stack" ,
"--allow-natives-syntax" ,
"--expose-gc" ,
"--no-concurrent-sweeping" ,
"--max-old-space-size=4096"
] ;
if ( nodeVersionMajor < 18 ) {
flags . push ( "--no-randomize-hashes" ) ;
}
if ( nodeVersionMajor < 20 ) {
flags . push ( "--no-scavenge-task" ) ;
}
return flags ;
} ;
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 CODSPEED = typeof process . env . CODSPEED !== "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 ;
}
if ( revList [ 3 ] ) {
return revList [ 3 ] ;
}
return revList [ 1 ] ;
}
2025-04-22 20:42:33 +08:00
/ * *
* @ param { ( string | undefined ) [ ] } revList rev list
* @ returns { Promise < string > } base
* /
2025-04-22 02:40:24 +08:00
async function getBase ( 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" ] ) ;
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" ) ;
}
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 ( ) {
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-04-29 23:45:18 +08:00
if ( CODSPEED ) {
return [
{
name : "HEAD" ,
rev : head
}
] ;
}
2025-04-22 02:40:24 +08:00
const base = await getBase ( revList ) ;
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" ) ;
const baselines = [ ] ;
try {
await fs . mkdir ( baselinesPath , { recursive : true } ) ;
} catch ( _err ) { } // eslint-disable-line no-empty
const baselineRevisions = await getBaselineRevs ( ) ;
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-04-29 23:45:18 +08:00
webpack : async config => {
const webpack = (
await import (
path . resolve (
baselinePath ,
` ./lib/index.js?nocache= ${ Math . random ( ) } `
)
)
) . default ;
await new Promise ( ( resolve , reject ) => {
const warmupCompiler = webpack ( config , ( err , _stats ) => {
if ( err ) {
reject ( err ) ;
return ;
}
warmupCompiler . purgeInputFileSystem ( ) ;
resolve ( ) ;
} ) ;
} ) ;
return webpack ;
}
2025-04-22 02:40:24 +08:00
} ) ;
}
const baselineRevision = baselineInfo . rev ;
const baselinePath = path . resolve ( baselinesPath , baselineRevision ) ;
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-04-29 23:45:18 +08:00
async function registerBenchmarks ( suite , test , baselines ) {
for ( const baseline of baselines ) {
const outputDirectory = path . join (
_ _dirname ,
"js" ,
"benchmark" ,
` baseline- ${ baseline . name } ` ,
test
) ;
const testDirectory = path . join ( casesPath , test ) ;
const config =
(
await import (
path . join ( testDirectory , ` webpack.config.js?nocache= ${ Math . random ( ) } ` )
)
) . default || { } ;
config . mode = config . mode || "production" ;
config . output = config . output || { } ;
if ( ! config . context ) config . context = testDirectory ;
if ( ! config . output . path ) config . output . path = outputDirectory ;
const suiteName = ` benchmark " ${ test } " ${ CODSPEED ? "" : ` ${ baseline . name } ( ${ baseline . rev } ) ` } ` ;
const webpack = await baseline . webpack ( config ) ;
suite . add ( suiteName , {
baseTestName : test ,
defer : true ,
fn ( deferred ) {
const compiler = webpack ( config , ( err , stats ) => {
compiler . purgeInputFileSystem ( ) ;
2025-04-22 02:40:24 +08:00
2025-04-29 23:45:18 +08:00
if ( err ) {
throw err ;
}
2025-04-22 02:40:24 +08:00
2025-04-29 23:45:18 +08:00
if ( stats . hasErrors ( ) ) {
throw new Error ( stats . toString ( ) ) ;
}
2025-04-22 02:40:24 +08:00
2025-04-29 23:45:18 +08:00
deferred . resolve ( ) ;
} ) ;
2025-04-22 02:40:24 +08:00
}
} ) ;
}
2025-04-29 23:45:18 +08:00
}
const suite = withCodSpeed (
new Benchmark . Suite ( {
maxTime : 30 ,
initCount : 1 ,
onError : event => {
throw new Error ( event . error ) ;
}
} )
) ;
const casesPath = path . join ( _ _dirname , "benchmarkCases" ) ;
const tests = [ ] ;
for ( const folder of await fs . readdir ( casesPath ) ) {
if ( folder . includes ( "_" ) ) {
continue ;
}
try {
await fs . access (
path . resolve ( casesPath , folder , "webpack.config.js" ) ,
constants . R _OK
) ;
} catch ( _err ) {
continue ;
}
tests . push ( folder ) ;
}
for ( const test of tests ) {
await registerBenchmarks ( suite , test , baselines ) ;
}
const statsByTests = new Map ( ) ;
suite . on ( "cycle" , event => {
const target = event . target ;
const stats = target . stats ;
const n = stats . sample . length ;
const nSqrt = Math . sqrt ( n ) ;
const z = tDistribution ( n - 1 ) ;
stats . sampleCount = stats . sample . length ;
stats . minConfidence = stats . mean - ( z * stats . deviation ) / nSqrt ;
stats . maxConfidence = stats . mean + ( z * stats . deviation ) / nSqrt ;
stats . text = ` ${ target . name } ${ Math . round ( stats . mean * 1000 ) } ms ± ${ Math . round (
stats . deviation * 1000
) } ms [ $ { Math . round ( stats . minConfidence * 1000 ) } ms ; $ { Math . round (
stats . maxConfidence * 1000
) } ms ] ` ;
const baseTestName = target . baseTestName ;
const allStats = statsByTests . get ( baseTestName ) ;
if ( ! allStats ) {
console . log ( String ( target ) ) ;
statsByTests . set ( baseTestName , [ stats ] ) ;
return ;
}
allStats . push ( stats ) ;
const headStats = allStats [ 0 ] ;
const baselineStats = allStats [ 1 ] ;
console . log (
` Benchmark " ${ baseTestName } " result: ${ headStats . text } is ${ Math . round (
( baselineStats . mean / headStats . mean ) * 100 - 100
) } % $ { baselineStats . maxConfidence < headStats . minConfidence ? "slower than" : baselineStats . minConfidence > headStats . maxConfidence ? "faster than" : "the same as" } $ { baselineStats . text } `
) ;
} ) ;
suite . run ( { async : true } ) ;
2025-04-22 02:40:24 +08:00
2025-04-29 23:45:18 +08:00
suite . on ( "complete" , ( ) => {
remove ( baselinesPath ) ;
2025-04-22 02:40:24 +08:00
} ) ;