hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

(小ネタ) Vitest でベンチマークテストする

どうもみゅーとんです.

小ネタです.

hacomonoは GitHub の hacomono-lib という organization にて, OSS を公開しているのですが,
なんとなく作っているロジックが多重ループになっていて計算量が多そうだ, と思い, そのパフォーマンス計測を行うことにしました.

概要

3 行でまとめ

  • Vitest には標準でベンチマークテストを行う仕組みがある
  • ただし experimental (v1.2.x 時点)
  • ベンチマークテストには tinybench を利用している

ここで話題にしないこと

  • 計算量の概念
  • パフォーマンス改善のためのコーディング


背景

インシデント未遂を引き起こしてしまいました.

hacomono-lib で公開している OSS の json-origami にて, パッチバージョンアップとして, たった1行変更するだけのバグ修正を行いました.

この修正が計算量を膨大に引き上げてしまい, プロダクトのコードでパフォーマンスが大幅劣化してしまいました.

暫定対処として, 利用する json-origami のバージョンを下げるなどの対応はしました.

恒久対処をするためには, 速度が改善されたかどうかを計測する仕組みが前提として必要でした.


実践

Vitest でパフォーマンステストを書く

まず, vitest.config.ts に以下のような修正を加えます.

今回は test/* フォルダ配下にある *.bench.ts ファイルをベンチマークテストを書くファイルとしています.

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    // これを追加
    benchmark: {
      include: ['test/**/*.bench.(js|ts)']
    }
  },
})

ベンチマークテストとして, 以下のようなコードを書きます.

import { bench, describe } from 'vitest'
import { createObject } from './test/utils'
import { modifyObject } from './src/modifyObject'

describe('benchmark test: "modifyObject"', () => {
  // ベンチマークテスト対象外にしたいコードは外に書く
  // createObject は例として 500 個値のある json object 作る関数として想定している.
  const object500 = createObject(500)

  bench('when use 500 size object', () => {
    modifyObject(object500)
  })
})

Vitest の通常のテストと同時に実行できないため, 以下のコマンドを package.json に追加しておくとよいです.

{
  "scripts": {
    "bench": "vitest bench"
  }
}


仕組み

vitest (v1.2時点) では内部で tinybench が使われており, bench 関数の第2 引数の関数を対象に, tinybench でパフォーマンス計測を行っています.

※ experimental なため, 変わる可能性があります.

上記のコードでは省略していますが, bench 関数は第 3 引数で tinybench のオプションを指定できるようになってます. 詳細は以下のとおり.

github.com

基本的には, ベンチマークテストしたい最小限のコードを第 2 引数にすれば良さそうです.

上記のコードだと, テストに関係ない “モックデータを作る処理” がbench の外に記載してあることで, 余計な処理をテスト対象に含めないようにしてます.

第 3 引数には time (default 500) iterations (default 10) の 2 パラメータがあり, テスト開始から合計 time ms 経過するまでは何度も実行し計測しますが, この時間を超えて iterations 回数に満たない場合は, 時間超過しつつ必ず iterations 回実施するようになっています.

結果の読み方

以下は json-origami でパフォーマンステストを実施した結果の一部です

✓ test/twist.bench.ts (6) 1840ms
   ✓ twist with light object (3) 1836ms
     name                                                                hz     min      max    mean     p75      p99     p995     p999     rme  samples
   · twist (complex object including 100 values, twist 10% of keys)  264.30  0.8337  13.3483  3.7835  4.3818  10.4968  13.3483  13.3483  ±7.74%      133   fastest
   · twist (complex object including 100 values, twist 50% of keys)  215.81  1.3680  10.4523  4.6338  5.3692   9.8662  10.4523  10.4523  ±6.44%      108
   · twist (complex object including 100 values, twist 90% of keys)  162.60  2.9027  10.6133  6.1502  6.9685  10.6133  10.6133  10.6133  ±4.95%       82   slowest

ちょっと横に長いですが, それぞれ項目の意味は以下の通り

  • hz .. 1 秒間にこの処理を実行できる回数
  • min .. 計測したなかで最短だった時間 (ms)
  • max .. 計測したなかで最長だった時間 (ms)
  • mean .. 標本平均 (ms)
  • p75 .. 75 パーセンタイル (ms)
  • p99 .. 99 パーセンタイル (ms)
  • p995 .. 99.5 パーセンタイル (ms)
  • p999 .. 99.9 パーセンタイル (ms)
  • rme .. 相対誤差 (%)
  • samples .. 標本数

hz?

周波数の Hz と同じ意味合いだと思います. 数が多いほど同じ時間での実行可能数が多いことを意味します.

標本平均?

一般的に平均と呼ばれたりしますが, 統計学上では標本平均と呼ばれたりします.
全標本を足し合わせて, 標本数で割った数です.

パーセンタイル?

小さい方から数えて指定 % の位置にある標本の値を指しています.

ベンチマークテストの結果においては, 例えば p75 であれば, 実行時の 75% はその値以下の時間で処理が完了する意味合いと捉えてよさそうです.

上記の実施結果の例をみると p75 p99 と差が開いていて, 結果が分散している様子が伺えるかと思います. 入力するオブジェクトをテスト毎に変更しているため, 分散する結果になったのかなと思っています.

(後のリファクタリングで安定化しました)

相対誤差?

一般的には (実測値 - 理論値) / 理論値 で表される, 理論値に対する誤差の割合ですが, tinybench においては理論値として標本平均が使われます.

つまり, 平均値に対してどれだけ結果に誤差が生じるかが得られており, いいかえると計測時間がどれだけブレるかがわかります. 少ないほど どんな入力値に対しても近い実行時間が得られる ことがわかります.

標本数?

実行回数です.

指定した time (ms) の時間内に実行した回数が記載されます.

time を超えても iterations 回数を実施できなかった場合は, 時間超過して iterations 回数だけは必ず実施します. そのため, 最低値は iterations と同じです.

高ければ高いほど高い精度の結果が得られます.

まとめ

案外, しっかり理解すると, パフォーマンス改善に役立てそうです.

今回は プロダクトのコードを切り出した OSS でのパフォーマンス検証であるため,
そもそも切り出されたロジックの検証であって, 導入しやすかったかなと思います.


株式会社hacomonoでは一緒に働く仲間を募集しています。
採用情報や採用ウィッシュリストもぜひご覧ください!