◆ 関数を指定回数実行時の経過時間を取得
◆ 複数回計測して平均や中央値などの統計情報出力

ときどきどっちのやり方のほうが速いのかで速度比較したりしてるのですが console.time を使って出力されるものを数回見るくらいで 平均出したりとかまですることはあまりなくて けっこうテキトーな感じでした

PC の状態などでの変動も割とありますし やっぱり何回も実行して ブレの大きい最初の数回は捨てて平均値・中央値・最小値・最大値を出したりとかしたほうがいいかなと思ってちょっとしたツールを作りました

使い方

インポートした measure 関数の 1 つめの引数に関数の配列を渡します
2 つめの引数には 1 回あたりの計測で実行する回数を指定します
1 回あたりの実行時間が短すぎる場合に指定します

こういう感じで使えます

import { measure, measureStat } from "/path/to/measure.js"

const f1 = () => 100
const f2 = () => {
let r = 0
for(let i=0;i<100;i++) r += 1
return r
}

measure([f1, f2], 1_000_000)
// [4.184999968856573, 112.45500016957521]

measure([f1, f2], 1_000_000)
// [11.874999850988388, 115.07999990135431]

measure([f1, f2], 1_000_000)
// [17.380000092089176, 119.65500004589558]

これだと 指定回数の繰り返し実行にかかった時間を 1 回だけそれぞれの関数に対して計測したものです
何回も実行してそれぞれの結果を見てるのはいつもどおりです

計測を何度か行ってそれらの平均などの統計情報を見るには measureStat を使います
引数は一緒で大丈夫です
結果は console に出力されます

measureStat([f1, f2], 500_000)

measurement-example

最初の数回は安定しないので 5 回は直接表示して それらを除外したもので平均などの統計情報を求めます

min: 最小時間 (ms)
max: 最大時間 (ms)
avg: 平均値 (ms)
med: 中央値 (ms)
ctr: (最小 + 最大) ÷ 2 (ms)
stat_count: 統計情報に使用した計測数
cps: 1 秒あたりの実行回数 (avg を元に計算)

計測回数は measureStat の 3 つ目の引数で指定できます
3 つめの引数を省略すると 50 になって 統計情報は 45 件から求められます
この引数を指定することで統計情報を作るための計測数を調整可能です

4 つめの引数は console.table を使うかのフラグです
false にすれば普通に console.log されます

measureStat([f1, f2], 500_000, 25, false)
// {once_count: 500000}
// (5) [Array(2), Array(2), Array(2), Array(2), Array(2)]
// 0 {min: 4.664999898523092, max: 5.470000207424164, avg: 4.967999993823469, med: 4.927499918267131, ctr: 5.067500052973628, stat_count: 20, cps: 100644122.50837995}
// 1 {min: 54.52500004321337, max: 58.32000030204654, avg: 56.30149997305125, med: 56.12249975092709, ctr: 56.42250017262995, stat_count: 20, cps: 8880758.065758912}

コード

const measureCore = (fn, count) => {
const t = performance.now()
for(let i=0;i<count;i++) fn()
return performance.now() - t
}

export const measure = (fns, count) => {
const times = []
for(let i=0; i < fns.length; i++) {
times.push(measureCore(fns[i], count))
}
return times
}

export const measureStat = (fns, once_count, measure_count = 50, table = true) => {
const times = []
for(let i = 0; i < measure_count; i++) {
times.push(measure(fns, once_count))
}

const pre = 5
console.log({ once_count })
console.log(times.slice(0, pre))
if (times.length <= pre) return

const fn_times = Array(fns.length).fill().map(() => [])
for(const ts of times.slice(pre)) {
for(const [i, time] of ts.entries()) {
fn_times[i].push(time)
}
}

const table_data = []
for(const [i, ts] of fn_times.entries()) {
let sum = 0
let max = -Infinity
let min = Infinity
for(const time of ts) {
sum += time
if (max < time) max = time
if (min > time) min = time
}
const avg = sum / ts.length
ts.sort((a, b) => a - b)
const med = ts.length % 2 ? ts[~~(ts.length / 2)] : (ts[ts.length / 2] + ts[ts.length / 2 - 1]) / 2
const ctr = (min + max) / 2
const stat_count = measure_count - pre
const call_per_sec = once_count / (avg / 1000)
if (table) {
table_data.push({ min, max, avg, med, ctr, stat_count, cps: call_per_sec })
} else {
console.log(i, { min, max, avg, med, ctr, stat_count, cps: call_per_sec })
}
}
if (table) console.table(table_data)
return table_data
}