Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Optimization] Cache prev values in closure instead of recordPropMetadata #297

Open
Tracked by #293
yyx990803 opened this issue Dec 1, 2024 · 1 comment
Open
Tracked by #293

Comments

@yyx990803
Copy link
Member

Currently prop-setting helpers do this:

function setClass(el, value) {
  const prev = recordPropMetadata(el, 'class', value)
  if (value !== prev && (value || prev)) {
    el.className = value
  }
}

function recordPropMetadata(el, key, value) {
  const metadata = getMetadata(el)[0]
  const prev = metadata[key]
  if (prev !== value) metadata[key] = value
  return prev
}

function getMetadata(el) {
  return el.$$metadata || (el.$$metadata = [{}, {}])
}

recordPropMetadata has noticeable overhead on every call:

  • extra index / key access
  • extra array / object allocation

Changes Needed

For template

<div>
   <div :id="foo" :class="bar"></div>
</div>

Current codegen:

const n1 = /* ... */
_renderEffect(() => _setDOMProp(n1, "id", foo))
_renderEffect(() => _setClass(n1, bar))

Should be changed to (storing prev update values in the local closure):

const n1 = /* ... */
let _id, _cls
_renderEffect(() => _setDOMProp(n1, "id", _id, (_id = foo)))
_renderEffect(() => _setClass(n1, _cls, (_cls = bar)))

And in the relevant helpers, prev value should come from the argument instead of element metadata.

Benchmark

A simple benchmark that simulate a typical vapor update function:

<script type="module">
  import { Bench } from 'https://esm.sh/tinybench'

  function recordPropMetadata(el, key, value) {
    const metadata = getMetadata(el)[0]
    const prev = metadata[key]
    if (prev !== value) metadata[key] = value
    return prev
  }

  function getMetadata(el) {
    return el.$$metadata || (el.$$metadata = [{}, {}])
  }

  function setAttr(el, key, value) {
    const oldVal = recordPropMetadata(el, key, value)
    if (value !== oldVal) {
      if (value != null) {
        el.setAttribute(key, value)
      } else {
        el.removeAttribute(key)
      }
    }
  }

  function setAttr2(el, key, oldVal, value) {
    if (value !== oldVal) {
      if (value != null) {
        el.setAttribute(key, value)
      } else {
        el.removeAttribute(key)
      }
    }
  }

  function setClass(el, value) {
    const prev = recordPropMetadata(el, 'class', value)
    if (value !== prev && (value || prev)) {
      el.className = value
    }
  }

  function setClass2(el, prev, value) {
    if (value !== prev && (value || prev)) {
      el.className = value
    }
  }

  const updateOne = (() => {
    const n1 = document.createElement('div')
    const n2 = document.createElement('div')
    const n3 = document.createElement('div')
    const n4 = document.createElement('div')
    return val => {
      setClass(n1, val)
      setAttr(n1, 'id', val)
      setClass(n2, val)
      setAttr(n3, 'id', val)
      setClass(n4, val)
    }
  })()

  const updateTwo = (() => {
    const n1 = document.createElement('div')
    const n2 = document.createElement('div')
    const n3 = document.createElement('div')
    const n4 = document.createElement('div')
    let cls, id, cls2, id2, cls3
    return val => {
      setClass2(n1, cls, (cls = val))
      setAttr2(n1, 'id', id, (id = val))
      setClass2(n2, val, (cls2 = val))
      setAttr2(n3, 'id', val, (id2 = val))
      setClass2(n4, val, (cls3 = val))
    }
  })()

  const bench = new Bench({ name: 'old value handling for set ops', time: 100 })
  let i
  bench
    .add('in closure', () => {
      updateTwo(i++ % 5 ? 'foo' : 'bar')
    })
    .add('in metadata', () => {
      updateOne(i++ % 5 ? 'foo' : 'bar')
    })

  await bench.run()

  console.log(bench.name)
  const output = JSON.stringify(bench.table(), null, 2)
  document.getElementById('output').textContent = output
</script>

<pre id="output"></pre>

Result in Chrome:

[
  {
    "Task name": "in closure",
    "Latency average (ns)": "51.16 ± 6.19%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "19537649 ± 0.00%",
    "Throughput median (ops/s)": "19547644",
    "Samples": 1956719
  },
  {
    "Task name": "in metadata",
    "Latency average (ns)": "79.90 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "12505888 ± 0.00%",
    "Throughput median (ops/s)": "12515880",
    "Samples": 1251588
  }
]
@edison1105
Copy link
Member

<script type="module">
    import { Bench } from 'https://esm.sh/tinybench'
  
    function recordPropMetadata(el, key, value) {
      const metadata = getMetadata(el)[0]
      const prev = metadata[key]
      if (prev !== value) metadata[key] = value
      return prev
    }
  
    function getMetadata(el) {
      return el.$$metadata || (el.$$metadata = [{}, {}])
    }
  
    function setAttr(el, key, value) {
      const oldVal = recordPropMetadata(el, key, value)
      if (value !== oldVal) {
        if (value != null) {
          el.setAttribute(key, value)
        } else {
          el.removeAttribute(key)
        }
      }
    }
  
    function setAttr2(el, key, oldVal, value) {
      if (value !== oldVal) {
        if (value != null) {
          el.setAttribute(key, value)
        } else {
          el.removeAttribute(key)
        }
      }
    }

    function setAttr3(el, key, value) {
        if (value != null) {
          el.setAttribute(key, value)
        } else {
          el.removeAttribute(key)
        }
    }


  
    function setClass(el, value) {
      const prev = recordPropMetadata(el, 'class', value)
      if (value !== prev && (value || prev)) {
        el.className = value
      }
    }
  
    function setClass2(el, prev, value) {
      if (value !== prev && (value || prev)) {
        el.className = value
      }
    }

    function setClass3(el, value) {
      if ((value || prev)) {
        el.className = value
      }
    }
  
    const updateOne = (() => {
      const n1 = document.createElement('div')
      const n2 = document.createElement('div')
      const n3 = document.createElement('div')
      const n4 = document.createElement('div')
      return val => {
        setClass(n1, val)
        setAttr(n1, 'id', val)
        setClass(n2, val)
        setAttr(n3, 'id', val)
        setClass(n4, val)
      }
    })()
  
    const updateTwo = (() => {
      const n1 = document.createElement('div')
      const n2 = document.createElement('div')
      const n3 = document.createElement('div')
      const n4 = document.createElement('div')
      let cls, id, cls2, id2, cls3
      return val => {
        setClass2(n1, cls, (cls = val))
        setAttr2(n1, 'id', id, (id = val))
        setClass2(n2, val, (cls2 = val))
        setAttr2(n3, 'id', val, (id2 = val))
        setClass2(n4, val, (cls3 = val))
      }
    })()

    const updateThree = (() => {
      const n1 = document.createElement('div')
      const n2 = document.createElement('div')
      const n3 = document.createElement('div')
      const n4 = document.createElement('div')
      let cls, id, cls2, id2, cls3
      return val => {
        cls !== val && setClass3(n1,(cls = val))
        id !== val && setAttr3(n1, 'id',(id = val))
        cls2 !== val && setClass3(n2,(cls2 = val))
        id2 !== val && setAttr3(n3, 'id', (id2 = val))
        cls3 !== val && setClass3(n4, (cls3 = val))
      }
    })()
  
    const bench = new Bench({ name: 'old value handling for set ops', time: 100 })
    let i
    bench
      .add('in closure', () => {
        updateTwo(i++ % 5 ? 'foo' : 'bar')
      })
      .add('in metadata', () => {
        updateOne(i++ % 5 ? 'foo' : 'bar')
      })
      .add('in closure 2', () => {
        updateThree(i++ % 5 ? 'foo' : 'bar')
      })
  
    await bench.run()
  
    console.log(bench.name)
    const output = JSON.stringify(bench.table(), null, 2)
    document.getElementById('output').textContent = output
  </script>
  
  <pre id="output"></pre>

result in chrome

[
  {
    "Task name": "in closure",
    "Latency average (ns)": "59.88 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "16690096 ± 0.00%",
    "Throughput median (ops/s)": "16700090",
    "Samples": 1670009
  },
  {
    "Task name": "in metadata",
    "Latency average (ns)": "84.41 ± 6.21%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "11836568 ± 0.01%",
    "Throughput median (ops/s)": "11846530",
    "Samples": 1184653
  },
  {
    "Task name": "in closure 2",
    "Latency average (ns)": "56.18 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "17788545 ± 0.00%",
    "Throughput median (ops/s)": "17798540",
    "Samples": 1779854
  }
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants