2025-08-10 22:00:30 +08:00
import fs from "fs" ;
import fsp from "fs/promises" ;
import os from "os" ;
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-08-10 22:00:30 +08:00
import { Worker } from "jest-worker" ;
2025-07-03 17:06:45 +08:00
import { simpleGit } from "simple-git" ;
2025-08-10 22:00:30 +08:00
/** @typedef {import("./benchmarkCases/_helpers/benchmark.worker.mjs").BenchmarkResult} BenchmarkResult */
/** @typedef {import("./benchmarkCases/_helpers/benchmark.worker.mjs").Result} Result */
/ * *
* @ typedef { object } BaselineRev
* @ property { string } name baseline rev name
* @ property { string } rev baseline revision
* /
/ * *
* @ typedef { object } Baseline
* @ property { string } name baseline rev name
* @ property { string = } rev baseline revision
* @ property { string } path baseline path
* /
/ * *
* @ typedef { object } BenchmarkScenario
* @ property { string } name scenario name
* @ property { "development" | "production" } mode mode
* @ property { boolean = } watch watch mode
* /
/ * *
* @ typedef { object } BenchmarkTask
* @ property { string } id task id ( includes benchmark name and scenario name )
* @ property { string } benchmark benchmark name
* @ property { BenchmarkScenario } scenario scenario
* @ property { Baseline } baseline baseline
* /
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-08-10 22:00:30 +08:00
/ * *
* Gets V8 flags based on Node . js version
* @ returns { string [ ] } Array of V8 flags
* /
2025-04-29 23:45:18 +08:00
const getV8Flags = ( ) => {
const nodeVersionMajor = Number . parseInt (
2025-07-02 20:10:54 +08:00
process . version . slice ( 1 ) . split ( "." ) [ 0 ] ,
10
2025-04-29 23:45:18 +08:00
) ;
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 ;
} ;
2025-08-10 22:00:30 +08:00
/** @type {boolean} */
2025-05-14 19:46:56 +08:00
const LAST _COMMIT = typeof process . env . LAST _COMMIT !== "undefined" ;
2025-04-22 05:17:46 +08:00
2025-04-22 20:42:33 +08:00
/ * *
2025-08-10 22:00:30 +08:00
* Gets the HEAD commit
* @ param { ( string | undefined ) [ ] } revList Revision list from git
* @ returns { Promise < string > } HEAD commit hash
2025-04-22 20:42:33 +08:00
* /
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-08-10 22:00:30 +08:00
* Gets the BASE commit
* @ param { string } head HEAD commit hash
* @ param { ( string | undefined ) [ ] } revList Revision list from git
* @ returns { Promise < string > } BASE commit hash
2025-04-22 20:42:33 +08:00
* /
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
/ * *
2025-08-10 22:00:30 +08:00
* Gets baseline revisions for benchmarking
* @ returns { Promise < BaselineRev [ ] > } Array of baseline revisions
2025-04-22 20:42:33 +08:00
* /
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
/ * *
2025-08-10 22:00:30 +08:00
* Splits array into N chunks
* @ template T
* @ param { T [ ] } array Input array
* @ param { number } n Number of chunks
* @ returns { T [ ] [ ] } Array of chunks
2025-04-22 20:42:33 +08:00
* /
2025-08-10 22:00:30 +08:00
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-22 02:40:24 +08:00
}
2025-08-10 22:00:30 +08:00
return result ;
2025-04-22 02:40:24 +08:00
}
2025-08-10 22:00:30 +08:00
class BenchmarkRunner {
constructor ( ) {
/** @type {BenchmarkScenario[]} */
this . scenarios = [
{
name : "mode-development" ,
mode : "development"
} ,
{
name : "mode-development-rebuild" ,
mode : "development" ,
watch : true
} ,
{
name : "mode-production" ,
mode : "production"
2025-04-29 23:45:18 +08:00
}
2025-08-10 22:00:30 +08:00
] ;
/** @type {string} */
this . output = path . join ( _ _dirname , "./js" ) ;
/** @type {string} */
this . baselinesPath = path . join ( this . output , "benchmark-baselines" ) ;
/** @type {string} */
this . baseOutputPath = path . join ( this . output , "benchmark" ) ;
/** @type {string} */
this . casesPath = path . join ( _ _dirname , "benchmarkCases" ) ;
2025-04-22 02:40:24 +08:00
}
2025-08-10 22:00:30 +08:00
/ * *
* Initializes benchmark
* @ returns { Promise < Baseline [ ] > } Baselines
* /
async initialize ( ) {
const baselineRevisions = await getBaselineRevs ( ) ;
2025-04-22 02:40:24 +08:00
try {
2025-08-10 22:00:30 +08:00
await fsp . mkdir ( this . baselinesPath , { recursive : true } ) ;
2025-04-22 02:40:24 +08:00
} catch ( _err ) { } // eslint-disable-line no-empty
2025-08-10 22:00:30 +08:00
/** @type {Baseline[]} */
const baselines = [ ] ;
2025-04-22 02:40:24 +08:00
2025-08-10 22:00:30 +08:00
for ( const baselineInfo of baselineRevisions ) {
const baselineRevision = baselineInfo . rev ;
2025-04-22 02:40:24 +08:00
2025-08-10 22:00:30 +08:00
const baselinePath =
baselineRevision === undefined
? path . resolve ( _ _dirname , "../" )
: path . resolve ( this . baselinesPath , baselineRevision ) ;
2025-04-22 02:40:24 +08:00
2025-08-10 22:00:30 +08:00
try {
await fsp . access (
path . resolve ( baselinePath , ".git" ) ,
fsp . constants . R _OK
) ;
} catch ( _err ) {
try {
await fsp . mkdir ( baselinePath ) ;
} catch ( _err ) { } // eslint-disable-line no-empty
const gitIndex = path . resolve ( rootPath , ".git/index" ) ;
const index = await fsp . 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 fsp . writeFile ( gitIndex , index ) ;
} finally {
baselines . push ( {
name : baselineInfo . name ,
rev : baselineRevision ,
path : baselinePath
} ) ;
2025-08-12 07:41:06 +08:00
}
2025-08-10 22:00:30 +08:00
}
await fsp . rm ( this . baseOutputPath , { recursive : true , force : true } ) ;
2025-08-12 07:41:06 +08:00
2025-08-10 22:00:30 +08:00
return baselines ;
2025-05-15 23:43:29 +08:00
}
2025-08-10 22:00:30 +08:00
async createWorkerPool ( ) {
const cpus = Math . max ( 1 , os . availableParallelism ( ) - 1 ) ;
2025-05-01 04:52:14 +08:00
2025-08-10 22:00:30 +08:00
this . workerPool = new Worker (
path . join ( this . casesPath , "_helpers" , "/benchmark.worker.mjs" ) ,
{
exposedMethods : [ "run" ] ,
numWorkers : cpus ,
forkOptions : { silent : false , execArgv : getV8Flags ( ) }
}
) ;
2025-07-24 04:48:44 +08:00
}
2025-08-10 22:00:30 +08:00
/ * *
* Prepares benchmark tasks
* @ param { { BenchmarkTask [ ] } } benchmarkTasks all benchmark tasks
* @ returns { Promise < void > }
* /
async prepareBenchmarkTask ( benchmarkTasks ) {
for ( const task of benchmarkTasks ) {
const { benchmark } = task ;
const dir = path . join ( this . casesPath , benchmark ) ;
const optionsPath = path . resolve ( dir , "options.mjs" ) ;
let options = { } ;
if ( optionsPath && fs . existsSync ( optionsPath ) ) {
options = await import ( ` ${ pathToFileURL ( optionsPath ) } ` ) ;
2025-07-24 04:48:44 +08:00
}
2025-08-10 22:00:30 +08:00
if ( typeof options . setup !== "undefined" ) {
await options . setup ( ) ;
2025-07-24 04:48:44 +08:00
}
}
2025-08-10 22:00:30 +08:00
}
2025-07-24 04:48:44 +08:00
2025-08-10 22:00:30 +08:00
/ * *
* Create benchmark tasks
* @ param { string [ ] } benchmarks all benchmarks
* @ param { BenchmarkScenario [ ] } scenarios all scenarios
* @ param { Baseline [ ] } baselines all baselines
* @ returns { BenchmarkTask [ ] } benchmark tasks
* /
createBenchmarkTasks ( benchmarks , scenarios , baselines ) {
const benchmarkTasks = [ ] ;
for ( let benchIndex = 0 ; benchIndex < benchmarks . length ; benchIndex ++ ) {
for (
let scenarioIndex = 0 ;
scenarioIndex < scenarios . length ;
scenarioIndex ++
) {
const benchmark = benchmarks [ benchIndex ] ;
const scenario = scenarios [ scenarioIndex ] ;
benchmarkTasks . push ( {
id : ` ${ benchmark } - ${ scenario . name } ` ,
benchmark ,
scenario ,
baselines
} ) ;
}
2025-07-24 04:48:44 +08:00
}
2025-05-15 23:43:29 +08:00
2025-08-10 22:00:30 +08:00
return benchmarkTasks ;
2025-05-14 19:46:56 +08:00
}
2025-08-10 22:00:30 +08:00
/ * *
* Process benchmark results
* @ param { BenchmarkResult [ ] } benchmarkResults benchmark results
* /
processResults ( benchmarkResults ) {
/** @type {Map<string, Result[]>} */
const statsByTests = new Map ( ) ;
for ( const benchmarkResult of benchmarkResults ) {
for ( const results of benchmarkResult . results ) {
const collectBy = results . collectBy ;
const allStats = statsByTests . get ( collectBy ) ;
if ( ! allStats ) {
statsByTests . set ( collectBy , [ results ] ) ;
continue ;
}
allStats . push ( results ) ;
2025-05-14 19:46:56 +08:00
2025-08-10 22:00:30 +08:00
const firstStats = allStats [ 0 ] ;
const secondStats = allStats [ 1 ] ;
2025-05-14 19:46:56 +08:00
2025-08-10 22:00:30 +08:00
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 } `
) ;
}
}
2025-04-22 02:40:24 +08:00
}
2025-05-01 04:52:14 +08:00
2025-08-10 22:00:30 +08:00
async run ( ) {
const baselines = await this . initialize ( ) ;
await this . createWorkerPool ( ) ;
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 allBenchmarkCases = ( await fsp . readdir ( this . casesPath ) )
. filter (
( item ) =>
! item . includes ( "_" ) &&
( FILTER ? FILTER . test ( item ) : true ) &&
( NEGATIVE _FILTER ? ! NEGATIVE _FILTER . test ( item ) : true )
)
. sort ( ( a , b ) => a . localeCompare ( b ) ) ;
const benchmarkCases = allBenchmarkCases . filter (
( item ) => ! item . includes ( "-long" )
) ;
const longRunningBenchmarkCases = allBenchmarkCases . filter ( ( item ) =>
item . includes ( "-long" )
) ;
const i = Math . floor (
benchmarkCases . length / longRunningBenchmarkCases . length
) ;
for ( const [ index , value ] of longRunningBenchmarkCases . entries ( ) ) {
benchmarkCases . splice ( index * i , 0 , value ) ;
}
2025-05-01 04:52:14 +08:00
2025-08-10 22:00:30 +08:00
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 ] } `
) ;
}
2025-04-29 23:45:18 +08:00
2025-08-10 22:00:30 +08:00
const countOfBenchmarks = benchmarkCases . length ;
2025-04-29 23:45:18 +08:00
2025-08-10 22:00:30 +08:00
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-05-01 04:52:14 +08:00
2025-08-10 22:00:30 +08:00
const currentShardBenchmarkCases = splitToNChunks ( benchmarkCases , shard [ 1 ] ) [
shard [ 0 ] - 1
] ;
2025-04-29 23:45:18 +08:00
2025-08-10 22:00:30 +08:00
const benchmarkTasks = this . createBenchmarkTasks (
currentShardBenchmarkCases ,
this . scenarios ,
baselines
) ;
2025-05-15 23:43:29 +08:00
2025-08-10 22:00:30 +08:00
await this . prepareBenchmarkTask ( benchmarkTasks ) ;
2025-05-15 23:43:29 +08:00
2025-08-10 22:00:30 +08:00
try {
/** @type {BenchmarkResult[]} */
const benchmarkResults = await Promise . all (
benchmarkTasks . map ( ( task ) =>
this . workerPool . run ( {
task ,
casesPath : this . casesPath ,
baseOutputPath : this . baseOutputPath
} )
)
) ;
2025-05-14 19:46:56 +08:00
2025-08-10 22:00:30 +08:00
this . processResults ( benchmarkResults ) ;
} finally {
await this . workerPool . end ( ) ;
2025-05-14 19:46:56 +08:00
}
}
}
2025-08-10 22:00:30 +08:00
new BenchmarkRunner ( ) . run ( ) ;