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

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

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


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

```typescript
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のリークチェックのために書いたコード。タスク生成側で作成したオブジェクトと、タスク処理側で作成したオブジェクトをそれぞれ監視している

```typescript
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    │
    └─────────┴────────────┴────────────┴───────┴───────┘
```