vue3-core/benchmark/index.js

361 lines
8.5 KiB
JavaScript

// @ts-check
import path from 'node:path'
import { parseArgs } from 'node:util'
import { mkdir, rm, writeFile } from 'node:fs/promises'
import Vue from '@vitejs/plugin-vue'
import { build } from 'vite'
import connect from 'connect'
import sirv from 'sirv'
import { launch } from 'puppeteer'
import colors from 'picocolors'
import { exec, getSha } from '../scripts/utils.js'
// Thanks to https://github.com/krausest/js-framework-benchmark (Apache-2.0 license)
const {
values: {
skipLib,
skipApp,
skipBench,
vdom,
noVapor,
port: portStr,
count: countStr,
noHeadless,
devBuild,
},
} = parseArgs({
allowNegative: true,
allowPositionals: true,
options: {
skipLib: {
type: 'boolean',
short: 'v',
},
skipApp: {
type: 'boolean',
short: 'a',
},
skipBench: {
type: 'boolean',
short: 'b',
},
noVapor: {
type: 'boolean',
},
vdom: {
type: 'boolean',
short: 'v',
},
port: {
type: 'string',
short: 'p',
default: '8193',
},
count: {
type: 'string',
short: 'c',
default: '30',
},
noHeadless: {
type: 'boolean',
},
devBuild: {
type: 'boolean',
short: 'd',
},
},
})
const port = +(/** @type {string}*/ (portStr))
const count = +(/** @type {string}*/ (countStr))
const sha = await getSha(true)
if (!skipLib) {
await buildLib()
}
if (!skipApp) {
await rm('client/dist', { recursive: true }).catch(() => {})
vdom && (await buildApp(false))
!noVapor && (await buildApp(true))
}
const server = startServer()
if (!skipBench) {
await benchmark()
server.close()
}
async function buildLib() {
console.info(colors.blue('Building lib...'))
const options = {
cwd: path.resolve(import.meta.dirname, '..'),
stdio: 'inherit',
}
const buildOptions = devBuild ? '-df' : '-pf'
const [{ ok }, { ok: ok2 }, { ok: ok3 }, { ok: ok4 }] = await Promise.all([
exec(
'pnpm',
`run --silent build shared compiler-core compiler-dom compiler-vapor ${buildOptions} cjs`.split(
' ',
),
options,
),
exec(
'pnpm',
'run --silent build compiler-sfc compiler-ssr -f cjs'.split(' '),
options,
),
exec(
'pnpm',
`run --silent build vue-vapor ${buildOptions} esm-browser`.split(' '),
options,
),
exec(
'pnpm',
`run --silent build vue ${buildOptions} esm-browser-runtime`.split(' '),
options,
),
])
if (!ok || !ok2 || !ok3 || !ok4) {
console.error('Failed to build')
process.exit(1)
}
}
/** @param {boolean} isVapor */
async function buildApp(isVapor) {
console.info(
colors.blue(`\nBuilding ${isVapor ? 'Vapor' : 'Virtual DOM'} app...\n`),
)
process.env.NODE_ENV = 'production'
const CompilerSFC = await import(
'../packages/compiler-sfc/dist/compiler-sfc.cjs.js'
)
const prodSuffix = devBuild ? '.js' : '.prod.js'
/** @type {any} */
const TemplateCompiler = await import(
(isVapor
? '../packages/compiler-vapor/dist/compiler-vapor.cjs'
: '../packages/compiler-dom/dist/compiler-dom.cjs') + prodSuffix
)
const runtimePath = path.resolve(
import.meta.dirname,
(isVapor
? '../packages/vue-vapor/dist/vue-vapor.esm-browser'
: '../packages/vue/dist/vue.runtime.esm-browser') + prodSuffix,
)
const mode = isVapor ? 'vapor' : 'vdom'
await build({
root: './client',
base: `/${mode}`,
define: {
'import.meta.env.IS_VAPOR': String(isVapor),
},
build: {
minify: !devBuild && 'terser',
outDir: path.resolve('./client/dist', mode),
rollupOptions: {
onwarn(log, handler) {
if (log.code === 'INVALID_ANNOTATION') return
handler(log)
},
},
},
resolve: {
alias: {
'@vue/vapor': runtimePath,
'vue/vapor': runtimePath,
vue: runtimePath,
},
},
clearScreen: false,
plugins: [
Vue({
compiler: CompilerSFC,
template: { compiler: TemplateCompiler },
}),
],
})
}
function startServer() {
const server = connect().use(sirv('./client/dist')).listen(port)
printPort(port)
process.on('SIGTERM', () => server.close())
return server
}
async function benchmark() {
console.info(colors.blue(`\nStarting benchmark...`))
const browser = await initBrowser()
await mkdir('results', { recursive: true }).catch(() => {})
if (!noVapor) {
await doBench(browser, true)
}
if (vdom) {
await doBench(browser, false)
}
await browser.close()
}
/**
*
* @param {import('puppeteer').Browser} browser
* @param {boolean} isVapor
*/
async function doBench(browser, isVapor) {
const mode = isVapor ? 'vapor' : 'vdom'
console.info('\n\nmode:', mode)
const page = await browser.newPage()
page.emulateCPUThrottling(4)
await page.goto(`http://localhost:${port}/${mode}`, {
waitUntil: 'networkidle0',
})
await forceGC()
const t = performance.now()
for (let i = 0; i < count; i++) {
await clickButton('run') // test: create rows
await clickButton('update') // partial update
await clickButton('swaprows') // swap rows
await select() // test: select row, remove row
await clickButton('clear') // clear rows
await withoutRecord(() => clickButton('run'))
await clickButton('add') // append rows to large table
await withoutRecord(() => clickButton('clear'))
await clickButton('runLots') // create many rows
await withoutRecord(() => clickButton('clear'))
// TODO replace all rows
}
console.info(
'Total time:',
colors.cyan(((performance.now() - t) / 1000).toFixed(2)),
's',
)
const times = await getTimes()
const result =
/** @type {Record<string, typeof compute>} */
Object.fromEntries(Object.entries(times).map(([k, v]) => [k, compute(v)]))
console.table(result)
await writeFile(
`results/benchmark-${sha}-${mode}.json`,
JSON.stringify(result, undefined, 2),
)
await page.close()
return result
function getTimes() {
return page.evaluate(() => /** @type {any} */ (globalThis).times)
}
async function forceGC() {
await page.evaluate(
`window.gc({type:'major',execution:'sync',flavor:'last-resort'})`,
)
}
/** @param {() => any} fn */
async function withoutRecord(fn) {
await page.evaluate(() => (globalThis.recordTime = false))
await fn()
await page.evaluate(() => (globalThis.recordTime = true))
}
/** @param {string} id */
async function clickButton(id) {
await page.click(`#${id}`)
await wait()
}
async function select() {
for (let i = 1; i <= 10; i++) {
await page.click(`tbody > tr:nth-child(2) > td:nth-child(2) > a`)
await page.waitForSelector(`tbody > tr:nth-child(2).danger`)
await page.click(`tbody > tr:nth-child(2) > td:nth-child(3) > button`)
await wait()
}
}
async function wait() {
await page.waitForSelector('.done')
}
}
async function initBrowser() {
const disableFeatures = [
'Translate', // avoid translation popups
'PrivacySandboxSettings4', // avoid privacy popup
'IPH_SidePanelGenericMenuFeature', // bookmark popup see https://github.com/krausest/js-framework-benchmark/issues/1688
]
const args = [
'--js-flags=--expose-gc', // needed for gc() function
'--no-default-browser-check',
'--disable-sync',
'--no-first-run',
'--ash-no-nudges',
'--disable-extensions',
`--disable-features=${disableFeatures.join(',')}`,
]
const headless = !noHeadless
console.info('headless:', headless)
const browser = await launch({
headless: headless,
args,
})
console.log('browser version:', colors.blue(await browser.version()))
return browser
}
/** @param {number[]} array */
function compute(array) {
const n = array.length
const max = Math.max(...array)
const min = Math.min(...array)
const mean = array.reduce((a, b) => a + b) / n
const std = Math.sqrt(
array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
)
const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
return {
max: round(max),
min: round(min),
mean: round(mean),
std: round(std),
median: round(median),
}
}
/** @param {number} n */
function round(n) {
return +n.toFixed(2)
}
/** @param {number} port */
function printPort(port) {
const vaporLink = !noVapor
? `\nVapor: ${colors.blue(`http://localhost:${port}/vapor`)}`
: ''
const vdomLink = vdom
? `\nvDom: ${colors.blue(`http://localhost:${port}/vdom`)}`
: ''
console.info(`\n\nServer started at`, vaporLink, vdomLink)
}