0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nodejsのオブジェクトが解放されたか監視したい

Last updated at Posted at 2025-04-26

背景

実装中のプログラムでメモリーリークの可能性があったため、解析する方法を必要になった。

疑わしい部分は絞り込めているため、プログラム全体ではなく特定オブジェクトが適切に解放されているかどうかを確認するプログラムを作成した。

実装

弱参照(WeakRef)を用いることで、監視対象のオブジェクトがGCにより回収されたかどうかを監視する。解放済みのオブジェクトを監視し続けることは意味がないので、解放が確認できた弱参照は削除していく

class MemoryLeakChecker {

    protected readonly objects: {
        [label: string]: {
            registered: number,
            refs: WeakRef<object>[] // Weak reference to objects
        }
    } = {}

    constructor(public readonly enabled: boolean = true) {
    }

    register<T extends object>(label: string, object: T): T {
        if (this.enabled) {
            if (!(label in this.objects)) {
                this.objects[label] = {
                    registered: 0,
                    refs: []
                }
            }
            this.objects[label].registered++
            this.objects[label].refs.push(new WeakRef(object))
        }
        return object
    }

    /*Remove WeakRef for released objects*/
    removeReleasedRefs() {
        if (this.enabled) {
            for (const label of Object.keys(this.objects)) {
                this.objects[label].refs = this.objects[label].refs.filter(r => r.deref() !== undefined)
            }
        }
    }

    summary() {
        const items: {
            label: string,
            registered: number,
            alive: number,
            freed: number,
        }[] = []
        if (this.enabled) {
            this.removeReleasedRefs()
            for (const label of Object.keys(this.objects)) {
                const {registered, refs} = this.objects[label]
                const alive = refs.length
                const freed = registered - refs.length
                items.push({
                    label,
                    registered,
                    alive,
                    freed
                })
            }
        }
        return items
    }
}

使い方

MemoryLeakCheckerはグローバルに作成し、監視対象となるオブジェクトを登録して利用することを想定している。

メモリ監視が必要ない場合には、MemoryLeakChecker初期化時にfalseを与えることで機能を一元的に無効化できる。

サンプルコード

ya-synのリークチェックのために書いたコード。タスク生成側で作成したオブジェクトと、タスク処理側で作成したオブジェクトをそれぞれ監視している

const checker = new MemoryLeakChecker()
const m = checker.register.bind(checker)
setInterval(() => {
    console.table(checker.summary())
}, 1000).unref()

describe("MemoryLeak", () => {
    test('Executor', async () => {
        const size = 5000000
        const sp = new SynchronizerProvider()
        const lock = sp.createSynchronizer({
            maxConcurrentExecution: 3
        })
        await sp.executeTasks({
            maxTasksInFlight: 10,
            maxTasksInExecution: 5,
            taskSource: mergeAsyncGenerators([
                async function* () {
                    for (let i = 0; i < 1000; i++) {
                        yield {
                            executionId: `${i}`,
                            task: m("task", new Array(size))
                        }
                    }
                }(),
            ]),
            taskExecutor: async ({executionId}) => {
                await lock.synchronized(async () => {
                    await new Promise(r => setTimeout(r, 1000))
                    console.log(`${executionId}:executed`)
                    return m("result", new Array(size))
                })
            }
        })
    }, 1000 * 60 * 60);
})

実行結果

  console.log
    ┌─────────┬──────────┬────────────┬───────┬───────┐
    │ (index) │ label    │ registered │ alive │ freed │
    ├─────────┼──────────┼────────────┼───────┼───────┤
    │ 0       │ 'task'   │ 34         │ 14    │ 20    │
    │ 1       │ 'result' │ 24         │ 4     │ 20    │
    └─────────┴──────────┴────────────┴───────┴───────┘

      at Timeout._onTimeout (test/MemoryLeak.test.ts:66:13)

gcを直接読んだ場合

expose-gcでgcを利用できるように設定する 

NODE_OPTIONS="--expose-gc" npm test test/MemoryLeak.test.ts

毎回GCした場合の実行ログ
処理中のメッセージ10件以外は綺麗に解放されている

    ┌─────────┬────────────┬────────────┬───────┬───────┐
    │ (index) │ label      │ registered │ alive │ freed │
    ├─────────┼────────────┼────────────┼───────┼───────┤
    │ 0       │ 'register' │ 43         │ 10    │ 33    │
    │ 1       │ 'execute'  │ 33         │ 0     │ 33    │
    └─────────┴────────────┴────────────┴───────┴───────┘
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?