index.js 8.6 KB

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