index.js 8.0 KB

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