うさぎのメモ帳

performance.mark() と performance.measure() が少し扱いづらい

2019年10月18日 - 2019年12月04日

JSで処理速度を計測するときに performance.mesure() を使うように成って間もないですが、これが少し扱いづらい。

performance.mark('a:start') // a:start でマークをして
// 何らかの処理
performance.mark('a:end') // a:end でマークをして
performance.measure('measure a:start to a:end', 'a:start', 'a:end') // a:start から a:end までの時間を計測

performance.mark('b:start') // b:start でマークをして
// 何らかの処理
performance.mark('b:end') // b:end でマークをして
performance.measure('measure b:start to b:end', 'b:start', 'b:end') // b:start から b:end までの時間を計測

console.log(performance.getEntriesByType('measure')) // PerformanceMeasure[] を出力

// [
//   {
//     duration: 0.014999999621068127
//     entryType: "measure"
//     name: "measure a:start to a:end"
//     startTime: 14.714999999341671
//   }, ...
// ]

特定の場所に名称でマークをして、範囲を指定して測定できるのは便利ですが、ひとつずつの処理の時間を計りたいことのほうが多いと思いました。

そこで例えばuseP()というパフォーマンスを計測するための関数があって、以下のように使えたら便利だなと。

const p = useP()

p.mark()
// 何らかの処理
p.measure('Time A')

p.mark()
// 何らかの処理
p.measure('Time B')

p.log() // console.log で出力
p.table() // console.table で出力

本当はp.mark()の時点で計測名をつけたほうが分かりやすいのですが、あまり元の仕様を崩したくはないので。
この仕様なら元の関数から名前の引数がなくなったと考えるだけで済みます。

ので、以下のようにつくりました。

const useP = () => {
  performance.clearMarks()
  performance.clearMeasures()

  let counter = 0

  const mark = () => {
    performance.mark(String(counter))
  }

  const measure = (name) => {
    performance.measure(name, String(counter))
    counter += 1
  }

  const log = () => {
    console.log(performance.getEntriesByType('measure'))
  }

  const table = () => {
    console.table(performance.getEntriesByType('measure'))
  }

  return {
    mark,
    measure,
    log,
    table
  }
}

window.useP = () => { ...で定義しておいて先に読み込んでおけば全体で使うこともできます。

Node環境なら、

※以下の useP では初回に実行される命令が遅く計測されてしまうため、計測結果には信憑性がありません。
のちに performance.now() を使わない方法を調べようと思います。

useP.js

const { performance } = require('perf_hooks')
module.exports = () => {
  let t = 0

  const start = () => {
    t = performance.now()
  }

  const end = () => {
    const end = performance.now()
    console.log(end - t)
    t = end
  }

  return {
    start,
    end
  }
}

script.js

const _ = require('lodash')
const useP = require('./useP.js')

const p = useP()

const data = []
for (let i = 0; i < 1000; i++) {
  data.push(i)
}

const dataA = []
const dataB = []
let dataC

p.start()
data.forEach((i) => {
  if(i % 2 === 0) dataA.push(i)
})
p.end() //=> 0.20699498057365417: 初回の実行は遅い

// dataAと同じ処理
p.start()
data.forEach((i) => {
  if(i % 2 === 0) dataB.push(i)
})
p.end() //=> 0.07255300879478455

p.start()
dataC = data.reduce((d, i) => {
  if(i % 2 === 0) d.push(i)
  return d
}, [])
p.end() //=> 0.13539502024650574

console.log(dataB.length === dataC.length) //=> true

とかでも使いやすかったです。

おまけ

最近 Vue 3 の書き方 (Composition API RFC) にものすごく影響を受けてて、useName()の関数を作っていく書き方がマイブーム。使うぞ!っていうのがわかりやすい。
Composition API と違ってwatchcomputedができないことを念頭におけば、比較的複雑な Vanilla JS 案件もuseNAME()で組むことができました。