index.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. // @ts-check
  2. import path from 'node:path'
  3. import { parseArgs } from 'node:util'
  4. import { mkdir, rm, writeFile } from 'node:fs/promises'
  5. import Vue from '@vitejs/plugin-vue'
  6. import { build } from 'vite'
  7. import connect from 'connect'
  8. import sirv from 'sirv'
  9. import { launch } from 'puppeteer'
  10. import colors from 'picocolors'
  11. import { exec, getSha } from '../../scripts/utils.js'
  12. import process from 'node:process'
  13. import readline from 'node:readline'
  14. // Thanks to https://github.com/krausest/js-framework-benchmark (Apache-2.0 license)
  15. const {
  16. values: {
  17. skipLib,
  18. skipApp,
  19. skipBench,
  20. vdom,
  21. noVapor,
  22. port: portStr,
  23. count: countStr,
  24. warmupCount: warmupCountStr,
  25. noHeadless,
  26. noMinify,
  27. reference,
  28. },
  29. } = parseArgs({
  30. allowNegative: true,
  31. allowPositionals: true,
  32. options: {
  33. skipLib: {
  34. type: 'boolean',
  35. short: 'l',
  36. },
  37. skipApp: {
  38. type: 'boolean',
  39. short: 'a',
  40. },
  41. skipBench: {
  42. type: 'boolean',
  43. short: 'b',
  44. },
  45. noVapor: {
  46. type: 'boolean',
  47. },
  48. vdom: {
  49. type: 'boolean',
  50. short: 'v',
  51. },
  52. port: {
  53. type: 'string',
  54. short: 'p',
  55. default: '8193',
  56. },
  57. count: {
  58. type: 'string',
  59. short: 'c',
  60. default: '30',
  61. },
  62. warmupCount: {
  63. type: 'string',
  64. short: 'w',
  65. default: '5',
  66. },
  67. noHeadless: {
  68. type: 'boolean',
  69. },
  70. noMinify: {
  71. type: 'boolean',
  72. },
  73. reference: {
  74. type: 'boolean',
  75. short: 'r',
  76. },
  77. },
  78. })
  79. const port = +(/** @type {string}*/ (portStr))
  80. const count = +(/** @type {string}*/ (countStr))
  81. const warmupCount = +(/** @type {string}*/ (warmupCountStr))
  82. const sha = await getSha(true)
  83. if (!skipLib && !reference) {
  84. await buildLib()
  85. }
  86. if (!skipApp && !reference) {
  87. await rm('client/dist', { recursive: true }).catch(() => {})
  88. vdom && (await buildApp(false))
  89. !noVapor && (await buildApp(true))
  90. }
  91. const server = startServer()
  92. if (!skipBench) {
  93. await benchmark()
  94. server.close()
  95. }
  96. async function buildLib() {
  97. console.info(colors.blue('Building lib...'))
  98. /** @type {import('node:child_process').SpawnOptions} */
  99. const options = {
  100. cwd: path.resolve(import.meta.dirname, '../..'),
  101. stdio: 'inherit',
  102. env: { ...process.env, BENCHMARK: 'true' },
  103. }
  104. const [{ ok }, { ok: ok2 }, { ok: ok3 }] = await Promise.all([
  105. exec(
  106. 'pnpm',
  107. `run --silent build shared compiler-core compiler-dom -pf cjs`.split(' '),
  108. options,
  109. ),
  110. exec(
  111. 'pnpm',
  112. 'run --silent build compiler-sfc compiler-ssr compiler-vapor -f cjs'.split(
  113. ' ',
  114. ),
  115. options,
  116. ),
  117. exec(
  118. 'pnpm',
  119. `run --silent build shared reactivity runtime-core runtime-dom runtime-vapor vue -f esm-bundler+esm-bundler-runtime`.split(
  120. ' ',
  121. ),
  122. options,
  123. ),
  124. ])
  125. if (!ok || !ok2 || !ok3) {
  126. console.error('Failed to build')
  127. process.exit(1)
  128. }
  129. }
  130. /** @param {boolean} isVapor */
  131. async function buildApp(isVapor) {
  132. console.info(
  133. colors.blue(`\nBuilding ${isVapor ? 'Vapor' : 'Virtual DOM'} app...\n`),
  134. )
  135. process.env.NODE_ENV = 'production'
  136. const CompilerSFC = await import(
  137. '../../packages/compiler-sfc/dist/compiler-sfc.cjs.js'
  138. )
  139. const runtimePath = path.resolve(
  140. import.meta.dirname,
  141. '../../packages/vue/dist/vue.runtime.esm-bundler.js',
  142. )
  143. const mode = isVapor ? 'vapor' : 'vdom'
  144. await build({
  145. root: './client',
  146. base: `/${mode}`,
  147. define: {
  148. 'import.meta.env.IS_VAPOR': String(isVapor),
  149. },
  150. build: {
  151. minify: !noMinify,
  152. outDir: path.resolve('./client/dist', mode),
  153. rollupOptions: {
  154. onwarn(log, handler) {
  155. if (log.code === 'INVALID_ANNOTATION') return
  156. handler(log)
  157. },
  158. },
  159. },
  160. resolve: {
  161. alias: {
  162. vue: runtimePath,
  163. },
  164. },
  165. clearScreen: false,
  166. plugins: [
  167. Vue({
  168. compiler: CompilerSFC,
  169. }),
  170. ],
  171. })
  172. }
  173. function startServer() {
  174. const server = connect()
  175. .use(sirv(reference ? './reference' : './client/dist', { dev: true }))
  176. .listen(port)
  177. printPort()
  178. process.on('SIGTERM', () => server.close())
  179. return server
  180. }
  181. async function benchmark() {
  182. console.info(colors.blue(`\nStarting benchmark...`))
  183. const browser = await initBrowser()
  184. await mkdir('results', { recursive: true }).catch(() => {})
  185. if (!noVapor) {
  186. await doBench(browser, true)
  187. }
  188. if (vdom) {
  189. await doBench(browser, false)
  190. }
  191. await browser.close()
  192. }
  193. /**
  194. * @param {boolean} isVapor
  195. */
  196. function getURL(isVapor) {
  197. return `http://localhost:${port}/${reference ? '' : isVapor ? 'vapor' : 'vdom'}/`
  198. }
  199. /**
  200. *
  201. * @param {import('puppeteer').Browser} browser
  202. * @param {boolean} isVapor
  203. */
  204. async function doBench(browser, isVapor) {
  205. const mode = reference ? `reference` : isVapor ? 'vapor' : 'vdom'
  206. console.info('\n\nmode:', mode)
  207. const page = await browser.newPage()
  208. page.emulateCPUThrottling(4)
  209. await page.goto(getURL(isVapor), {
  210. waitUntil: 'networkidle0',
  211. })
  212. await forceGC()
  213. const t = performance.now()
  214. console.log('warmup run')
  215. await eachRun(() => withoutRecord(benchOnce), warmupCount)
  216. console.log('benchmark run')
  217. await eachRun(benchOnce, count)
  218. console.info(
  219. 'Total time:',
  220. colors.cyan(((performance.now() - t) / 1000).toFixed(2)),
  221. 's',
  222. )
  223. const times = await getTimes()
  224. const result =
  225. /** @type {Record<string, typeof compute>} */
  226. Object.fromEntries(Object.entries(times).map(([k, v]) => [k, compute(v)]))
  227. console.table(result)
  228. await writeFile(
  229. `results/benchmark-${sha}-${mode}.json`,
  230. JSON.stringify(result, undefined, 2),
  231. )
  232. await page.close()
  233. return result
  234. async function benchOnce() {
  235. await clickButton('run') // test: create rows
  236. await clickButton('update') // partial update
  237. await clickButton('swaprows') // swap rows
  238. await select() // test: select row, remove row
  239. await clickButton('clear') // clear rows
  240. await withoutRecord(() => clickButton('run'))
  241. await clickButton('add') // append rows to large table
  242. await withoutRecord(() => clickButton('clear'))
  243. await clickButton('runlots') // create many rows
  244. await withoutRecord(() => clickButton('clear'))
  245. // TODO replace all rows
  246. }
  247. function getTimes() {
  248. return page.evaluate(() => /** @type {any} */ (globalThis).times)
  249. }
  250. async function forceGC() {
  251. await page.evaluate(
  252. `window.gc({type:'major',execution:'sync',flavor:'last-resort'})`,
  253. )
  254. }
  255. /** @param {() => any} fn */
  256. async function withoutRecord(fn) {
  257. const currentRecordTime = await page.evaluate(() => globalThis.recordTime)
  258. await page.evaluate(() => (globalThis.recordTime = false))
  259. await fn()
  260. await page.evaluate(
  261. currentRecordTime => (globalThis.recordTime = currentRecordTime),
  262. currentRecordTime,
  263. )
  264. }
  265. /** @param {string} id */
  266. async function clickButton(id) {
  267. await page.click(`#${id}`)
  268. await wait()
  269. }
  270. async function select() {
  271. for (let i = 1; i <= 10; i++) {
  272. await page.click(`tbody > tr:nth-child(2) > td:nth-child(2) > a`)
  273. await page.waitForSelector(`tbody > tr:nth-child(2).danger`)
  274. await page.click(`tbody > tr:nth-child(2) > td:nth-child(3) > a`)
  275. await wait()
  276. }
  277. }
  278. async function wait() {
  279. await page.waitForSelector('.done')
  280. }
  281. }
  282. /**
  283. * @param {Function} bench
  284. * @param {number} count
  285. */
  286. async function eachRun(bench, count) {
  287. for (let i = 0; i < count; i++) {
  288. readline.cursorTo(process.stdout, 0)
  289. readline.clearLine(process.stdout, 0)
  290. process.stdout.write(`${i + 1}/${count}`)
  291. await bench()
  292. }
  293. if (count === 0) {
  294. process.stdout.write('0/0 (skip)')
  295. }
  296. process.stdout.write('\n')
  297. }
  298. async function initBrowser() {
  299. const disableFeatures = [
  300. 'Translate', // avoid translation popups
  301. 'PrivacySandboxSettings4', // avoid privacy popup
  302. 'IPH_SidePanelGenericMenuFeature', // bookmark popup see https://github.com/krausest/js-framework-benchmark/issues/1688
  303. ]
  304. const args = [
  305. '--js-flags=--expose-gc', // needed for gc() function
  306. '--no-default-browser-check',
  307. '--disable-sync',
  308. '--no-first-run',
  309. '--ash-no-nudges',
  310. '--disable-extensions',
  311. `--disable-features=${disableFeatures.join(',')}`,
  312. ]
  313. const headless = !noHeadless
  314. console.info('headless:', headless)
  315. const browser = await launch({
  316. headless: headless,
  317. args,
  318. })
  319. console.log('browser version:', colors.blue(await browser.version()))
  320. return browser
  321. }
  322. /** @param {number[]} array */
  323. function compute(array) {
  324. const n = array.length
  325. const max = Math.max(...array)
  326. const min = Math.min(...array)
  327. const mean = array.reduce((a, b) => a + b) / n
  328. const std = Math.sqrt(
  329. array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
  330. )
  331. const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
  332. return {
  333. max: round(max),
  334. min: round(min),
  335. mean: round(mean),
  336. std: round(std),
  337. median: round(median),
  338. }
  339. }
  340. /** @param {number} n */
  341. function round(n) {
  342. return +n.toFixed(2)
  343. }
  344. function printPort() {
  345. const vaporLink = !noVapor
  346. ? `\n${reference ? `Reference` : `Vapor`}: ${colors.blue(getURL(true))}`
  347. : ''
  348. const vdomLink = vdom ? `\nvDom: ${colors.blue(getURL(false))}` : ''
  349. console.info(`\n\nServer started at`, vaporLink, vdomLink)
  350. }