Просмотр исходного кода

feat(playground): add benchmark

三咲智子 Kevin Deng 2 лет назад
Родитель
Сommit
670109e287

+ 116 - 0
playground/src/bench/App.vue

@@ -0,0 +1,116 @@
+<script setup lang="ts">
+import { ref, shallowRef } from 'vue'
+import { buildData } from './data'
+import { defer, wrap } from './profiling'
+
+const selected = ref<number>()
+const rows = shallowRef<
+  {
+    id: number
+    label: string
+  }[]
+>([])
+
+function setRows(update = rows.value.slice()) {
+  rows.value = update
+}
+
+const add = wrap('add', () => {
+  rows.value = rows.value.concat(buildData(1000))
+})
+
+const remove = wrap('remove', (id: number) => {
+  rows.value.splice(
+    rows.value.findIndex(d => d.id === id),
+    1,
+  )
+  setRows()
+})
+
+const select = wrap('select', (id: number) => {
+  selected.value = id
+})
+
+const run = wrap('run', () => {
+  setRows(buildData())
+  selected.value = undefined
+})
+
+const update = wrap('update', () => {
+  const _rows = rows.value
+  for (let i = 0; i < _rows.length; i += 10) {
+    _rows[i].label += ' !!!'
+  }
+  setRows()
+})
+
+const runLots = wrap('runLots', () => {
+  setRows(buildData(10000))
+  selected.value = undefined
+})
+
+const clear = wrap('clear', () => {
+  setRows([])
+  selected.value = undefined
+})
+
+const swapRows = wrap('swap', () => {
+  const _rows = rows.value
+  if (_rows.length > 998) {
+    const d1 = _rows[1]
+    const d998 = _rows[998]
+    _rows[1] = d998
+    _rows[998] = d1
+    setRows()
+  }
+})
+
+async function bench() {
+  for (let i = 0; i < 30; i++) {
+    rows.value = []
+    await defer()
+    await runLots()
+  }
+}
+</script>
+
+<template>
+  <h1>Vue.js Vapor Benchmark</h1>
+  <div
+    id="control"
+    style="display: flex; flex-direction: column; width: fit-content; gap: 6px"
+  >
+    <button @click="bench">Benchmark mounting</button>
+    <button id="run" @click="run">Create 1,000 rows</button>
+    <button id="runlots" @click="runLots">Create 10,000 rows</button>
+    <button id="add" @click="add">Append 1,000 rows</button>
+    <button id="update" @click="update">Update every 10th row</button>
+    <button id="clear" @click="clear">Clear</button>
+    <button id="swaprows" @click="swapRows">Swap Rows</button>
+  </div>
+  <div id="time"></div>
+  <table>
+    <tbody>
+      <tr
+        v-for="row of rows"
+        :key="row.id"
+        :class="{ danger: row.id === selected }"
+      >
+        <td>{{ row.id }}</td>
+        <td>
+          <a @click="select(row.id)">{{ row.label }}</a>
+        </td>
+        <td>
+          <button @click="remove(row.id)">x</button>
+        </td>
+        <td class="col-md-6"></td>
+      </tr>
+    </tbody>
+  </table>
+</template>
+
+<style>
+.danger {
+  background-color: red;
+}
+</style>

+ 75 - 0
playground/src/bench/data.ts

@@ -0,0 +1,75 @@
+let ID = 1
+
+function _random(max: number) {
+  return Math.round(Math.random() * 1000) % max
+}
+
+export function buildData(count = 1000) {
+  const adjectives = [
+    'pretty',
+    'large',
+    'big',
+    'small',
+    'tall',
+    'short',
+    'long',
+    'handsome',
+    'plain',
+    'quaint',
+    'clean',
+    'elegant',
+    'easy',
+    'angry',
+    'crazy',
+    'helpful',
+    'mushy',
+    'odd',
+    'unsightly',
+    'adorable',
+    'important',
+    'inexpensive',
+    'cheap',
+    'expensive',
+    'fancy',
+  ]
+  const colours = [
+    'red',
+    'yellow',
+    'blue',
+    'green',
+    'pink',
+    'brown',
+    'purple',
+    'brown',
+    'white',
+    'black',
+    'orange',
+  ]
+  const nouns = [
+    'table',
+    'chair',
+    'house',
+    'bbq',
+    'desk',
+    'car',
+    'pony',
+    'cookie',
+    'sandwich',
+    'burger',
+    'pizza',
+    'mouse',
+    'keyboard',
+  ]
+  const data = []
+  for (let i = 0; i < count; i++)
+    data.push({
+      id: ID++,
+      label:
+        adjectives[_random(adjectives.length)] +
+        ' ' +
+        colours[_random(colours.length)] +
+        ' ' +
+        nouns[_random(nouns.length)],
+    })
+  return data
+}

+ 61 - 0
playground/src/bench/profiling.ts

@@ -0,0 +1,61 @@
+// @ts-expect-error
+globalThis.doProfile = false
+// const defer = nextTick
+const ric =
+  typeof requestIdleCallback === 'undefined' ? setTimeout : requestIdleCallback
+export const defer = () => new Promise(r => ric(r))
+
+const times: Record<string, number[]> = {}
+
+export const wrap = (
+  id: string,
+  fn: (...args: any[]) => any,
+): ((...args: any[]) => Promise<void>) => {
+  if (import.meta.env.PROD) return fn
+  return async (...args) => {
+    const btns = Array.from(
+      document.querySelectorAll<HTMLButtonElement>('#control button'),
+    )
+    for (const node of btns) {
+      node.disabled = true
+    }
+    const doProfile = (globalThis as any).doProfile
+    await defer()
+
+    doProfile && console.profile(id)
+    const start = performance.now()
+    fn(...args)
+    await defer()
+    const time = performance.now() - start
+    doProfile && console.profileEnd(id)
+    const prevTimes = times[id] || (times[id] = [])
+    prevTimes.push(time)
+    const median = prevTimes.slice().sort((a, b) => a - b)[
+      Math.floor(prevTimes.length / 2)
+    ]
+    const mean = prevTimes.reduce((a, b) => a + b, 0) / prevTimes.length
+    const msg =
+      `${id}: min: ${Math.min(...prevTimes).toFixed(2)} / ` +
+      `max: ${Math.max(...prevTimes).toFixed(2)} / ` +
+      `median: ${median.toFixed(2)}ms / ` +
+      `mean: ${mean.toFixed(2)}ms / ` +
+      `time: ${time.toFixed(2)}ms / ` +
+      `std: ${getStandardDeviation(prevTimes).toFixed(2)} ` +
+      `: ${getStandardDeviation(prevTimes).toFixed(2)} ` +
+      `over ${prevTimes.length} runs`
+    console.log(msg)
+    document.getElementById('time')!.textContent = msg
+
+    for (const node of btns) {
+      node.disabled = false
+    }
+  }
+}
+
+function getStandardDeviation(array: number[]) {
+  const n = array.length
+  const mean = array.reduce((a, b) => a + b) / n
+  return Math.sqrt(
+    array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+  )
+}

+ 1 - 1
playground/src/main.ts

@@ -2,7 +2,7 @@ import { render, unmountComponent } from 'vue/vapor'
 import { createApp } from 'vue'
 import './style.css'
 
-const modules = import.meta.glob<any>('./*.(vue|js)')
+const modules = import.meta.glob<any>('./**/*.(vue|js)')
 const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
 
 mod.then(({ default: mod }) => {