hacomono TECH BLOG

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

(小ネタ) Vitest で Node.js の GC をテストする

どうもみゅーとんです.

小ネタです.

弊社は GitHub の hacomono-lib という organization にて, OSS を公開しているのですが,
なんとなく作っているロジックがちゃんとメモリ解放されているかが気になったので, そのテストを書く方法を探りました.

概要

3 行でまとめ

  • Node.js 実行時のオプションにて, ガベージコレクションを任意実行させる関数を有効化できる
  • メモリ解放されているかどうかは WeakRef を利用すると確認できる
  • Node.js 実行時のオプションを使って Vitest を実行できる

ここで話題にしないこと

  • メモリ管理 / ガベージコレクタについて
  • Vitest などのテスト導入について

背景

冒頭に記載したとおりですが, OSS を作っていてメモリがちゃんと開放されているかどうかが気になった次第です.

ライブラリのロジック内で, 複雑なロジックを実行するために必要なオブジェクトを作っており, これが処理完了後のガベージコレクタでちゃんとメモリが開放されているかどうか, 気になりませんかね.

私は気になりました.

実践

テストを書くためのヒント

ここでつかえるのは WeakRef です. developer.mozilla.org

WeakRef については, この記事が最も詳細に解説してくれているので, 詳細は省きます. qiita.com

要は, ガベージコレクタが効いた時点で参照がなければ, undefined になる値です.

内部処理で作ったオブジェクトなどを WeakRef で扱っておき, 参照がなくなった後のガベージコレクタの実施後, これが undefined になっていることが確認できていれば OK と判断してよいでしょう.

逆にこれが undefined になっていなければ, それは参照が残っている証拠であり, メモリリークが起きる原因になりえます.

ガベージコレクタを実施させる

基本的にガベージコレクタは内部で勝手に動くものであるため, 任意実行させることはできません.
これではテストができないため, ガベージコレクションを強制的に実行させる必要があります.

実行させるには global.gc() という関数をコールすれば OK です.
ただ, これは基本的に隠された機能で, NodeJS 実行時に --expose-gc オプションを設定する必要があります.

例:

node --expose-gc ./index.js

テストを書く

上記をふまえてテストを書いてみます.

import { it, expect } from 'vitest'

it('gc test', () => {
  let source = {}

  // 開放されるべきオブジェクトを取得し, weakRef につっこんでおく.
  // source = null のときに開放されることを期待する
  const target = new WeakRef(getTargetObject(source))

  expect(target.deref()).not.toUndefined()

  source = null

  // ガベージコレクションを強制実行する
  global.gc()

    expect(target.deref()).toUndefined()
})

package.json も以下のようにしておきます

{
    "scripts": {
    "test": "node --expose-gc ./node_modules/.bin/vitest"
  }
}

こんな感じ. 結構簡単に書けました.

実際のコードはこちら

上記を踏まえて実際に GC をテストしているコードはこちらです.

テストコードでは, json の変換処理が完了した後に, 使わなくなった変数がちゃんと開放されているかどうかをテストしています. github.com

まとめ

OSS 周りだと処理時間やメモリ効率のパフォーマンスが気になると思いますが,
これをテストに書けているとだいぶうれしいのではないでしょうか.

品質の高い OSS を今後も目指して作っていきます.


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